Your First Unit Test Using Angular CLI, Karma, and WebStorm
This is a guest blog post prepared by Pete Heard.
Pete is a full-stack JavaScript developer who has spent over a decade learning to craft robust software using Test Driven Development and advanced Object Oriented Design. He is the founder of Logic Room, a consultancy that helps organisations create enterprise web and mobile software.
In this article we will use WebStorm 2016.3 with its built-in support for the Angular CLI and Karma test runner. We will write a single unit test which will show you how to mock a service and check that your component works before integrating your changes!
What we will cover:
Setting up your application
1. Install Node.js
Okay, first off let’s make sure you have downloaded and upgraded to the latest and greatest of Node.js by going here.
2. Install Angular CLI
Angular CLI is a wrapper around some tools (for example, Webpack) that you need to create, build and deploy an Angular 2 application. WebStorm offers an integration with Angular CLI that allows you to create a new Angular 2 project and generate new components, directives, and services from the IDE.
We want to get a shiny new copy of Angular CLI on our machine, so enter the following into a terminal window:
npm install -g angular-cli
3. Create an Angular 2 project with Angular CLI
Open WebStorm and click Create new project on the IDE Welcome screen. You will be given an option to start a new Angular CLI project.
Choose this option, fill in the details and WebStorm will kick off a CLI command ng init --name=MyFirstTest
. Wait for WebStorm and the CLI to finish (notice the output in the tool window for indication and a line which reads ‘done’).
4. Set up Karma
Karma is a JavaScript test runner which means it will read code in a certain format (in our case written in Jasmine) and run it as a suite of tests. WebStorm provides neat integration with Karma, and our Angular project already has an initial configuration required to use Karma.
This setup is done through the common interface of the Karma configuration file called karma.conf.js. This file will tell Karma which locations the tests are in, which reporting mechanism to use, and which browser to use to execute the tests. Here is an example Karma config file from the Angular CLI project:
///... basePath: '', frameworks: ['jasmine', 'angular-cli'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-remap-istanbul'), require('angular-cli/plugins/karma') ], files: [ { pattern: './src/test.ts', watched: false } ], preprocessors: { './src/test.ts': ['angular-cli'] }, remapIstanbulReporter: { reports: { html: 'coverage', lcovonly: './coverage/coverage.lcov' } }, angularCli: { config: './angular-cli.json', environment: 'dev' }, ///...
Now we need to configure WebStorm to invoke Karma and pass it the config when we want to run our tests. This is really easy – we need to create a new Karma run/debug configuration. Go to Run – Edit Configurations from the menu.
Next, click the plus icon and select Karma from the list.
Enter the name as Test, then choose the configuration file (project/karma.conf.js). In the box that says Karma package, make sure you set the correct path to the Karma module in the project’s node_module folder.
Click OK to apply those settings and exit. Then simply click Run – Test and your tests will execute from WebStorm!
We have now finished configuring WebStorm for use with Angular 2 and have a simple project ready to go.
Writing Our First Unit Test (And Stubbing a Service)
We are ready to write our test and some code! We are going to create a very simple unit test which will check that some value is extracted from an Angular 2 service and that this value is then attached to our component (which is known as a fixture
1. Add The Skeleton Service and its Test Double
Make two files my-service.ts and my-fake-service.ts and place them in the app folder. The code for each one is below.
src/app/my-service.ts – the skeleton of the real service is needed for type safety!
import { Injectable } from '@angular/core'; @Injectable() export class MyService{ getMessage(){} }
src/app/my-fake-service.ts – the fake service is going to help us check that our component works.
import { Injectable } from '@angular/core'; @Injectable() export class MyFakeService{ getMessage(){ return 'fake service'; } }
2. Update the Test File with Dependencies and a Test
Since Angular 1 we have seen the use of modules. These are high-level containers that house our individual features. Angular 2 has this concept too and in order to test any component we need to load it into a module beforehand. This is done (in part) by a handy built-in helper class called TestBed.
In the file src/app/app.component.spec.ts, we need to stub the dependency for the service in the TestBed configuration and then we will add the unit test.
We aren’t going to dig around in the view of the component to make our assertions. Instead we are going to check the instance of the component as this will be cleaner. We will check the componentinstance
variable.
We are going to add our imports, new test and a provider declaration. The code listing below shows where to add them. Your code should look exactly like the following:
src/app/app.component.spec.ts
import { TestBed, async } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { MyService } from './my-service' import { MyFakeService } from './my-fake-service' describe('AppComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ declarations: [ AppComponent ], providers:[{provide:MyService,useClass:MyFakeService}] // --> new code }); TestBed.compileComponents(); }); it('should create the app', async(() => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; expect(app).toBeTruthy(); })); it(`should have as title 'app works!'`, async(() => { let fixture = TestBed.createComponent(AppComponent); let app = fixture.debugElement.componentInstance; expect(app.title).toEqual('app works!'); })); it('should render title in a h1 tag', async(() => { let fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); let compiled = fixture.debugElement.nativeElement; expect(compiled.querySelector('h1').textContent).toContain('app works!'); })); it('should attach message from service to component', async(() => { let fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); expect(fixture.componentInstance.message).toBe('fake service'); })); });
Tip: on large-scale projects, avoid digging around in views. Instead, create clean objects (viewmodels) that sit on your component and then check them, as this will keep your tests focused and increase the lifespan of your test suite. The views can be tested in isolation.
Once you have added this new code, you should see that the message
property on the component is red. Hooray! We are at the red part of our red/green refactor cycle. You can run the test now and it will fail. In fact technically it won’t ever run since it won’t pass the compile stage, and a test that doesn’t compile is the same as a failing test!
Tip: Enable Auto-run option in the Karma tool window (second icon in the left pane) to re-run tests automatically as you make changes in your code.
3. Make the Test Pass (and Compile)
First, we need to get rid of these compile errors. Let’s start by adding a message property into our component called
message
.
src/app/app.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app works!'; public message : string; }
Go back and run the test. Our test is still looking for something that doesn’t exist. Notice that we get the following:
Expected undefined to be 'fake service'. at webpack:///Users/peteheard/Desktop/MyFirstTest/src/app/app.component.spec.ts:48:46
Okay, so now let’s go in and actually get the test passing. We do this by injecting our service into the component. But to do that, we need to also import the type. Your code should now look exactly as follows (notice the added reference to the service):
src/app/app.component.ts
import { Component } from '@angular/core'; import { MyService } from './my-service' @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app works!'; public message : string; constructor(myService : MyService){ this.message = myService.getMessage(); } }
Note: At this point you might be wondering why this.message
in src/app/app.component.ts” is highlighted red. The reason is that the TypeScript service doesn’t yet know what the return type is and can’t tell at the moment that we have only implemented test code. When we wire up our final dependency, this will disappear.
Integrating Your Changes
Okay, you are now a bonafide TDDist, and when you do TDD well you should just be able to fire up your application and it will work first time. But let’s remember that we configured our dependency from a test context. We still need to wire up its real world counterpart. This involves 2 steps:
STEP 1: Update the real service my-service.ts to actually return something from its
getMessage
function.
src/app/my-service.ts
import { Injectable } from '@angular/core'; @Injectable() export class MyService{ getMessage(){ return 'real service'; } }
In the real world we wouldn’t have much untested code (such as with the real service). We are just learning how to mock/stub the service.
STEP 2: Add the dependency to the app.module.ts.
src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; import { MyService } from './my-service' @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, FormsModule, HttpModule ], providers: [MyService], bootstrap: [AppComponent] }) export class AppModule { }
STEP 3: Add The Property Onto The View
Finally, just add the property onto the view:
src/app/app.component.html
<h1> {{title> {{message}} </h1>
Now just use WebStorm integrated npm menu to start the application. On the left-hand side double-click start in the npm tool window.
The application will launch. Then go to the url as shown below.
Conclusion
In this article we have learned a standardized and consistent way to create new Angular 2 using Angular CLI in WebStorm and to write and run unit tests with Karma.
Then we had a little TDD adventure and saw how to use a test double in place of a real service using Dependency Injection. We checked that an Angular 2 component was working with a stub which was set up from the test context. Then we wired up our real dependency and integrated our changes and ran the browser to check the final output.
I hope you have enjoyed this article. Happy clean-coding!