You've been tasked to deliver a high-quality, well-tested dashboard to track The Grid's most prominent hackers!
Details
This project was generated with Angular CLI version 1.0.6.
- Git
- Node (at least 6.10)
- npm (at least 3.10)
- latest Google Chrome
- latest Google Chrome Canary (important!)
- GitHub account
- Visual Studio Code editor
You will need to do the following:
- fork this repo to your GitHub account
- clone your fork locally
- Globally install the Angular CLI:
npm install -g @angular/cli
- inside
fluent-angular-testing-workshop
, install dependencies:npm install
The following commands should work:
npm start
: should open your browser and display the app we will be working with:
npm test
: should yield output similar to this (no errors):
git checkout -b solution
We will be working on a new branch and working through the modules. In the last module, we will be opening a pull request and using TravisCI to run our builds.
Details
- A BDD framework for JS code
- standalone, no DOM required
- Clean syntax: describe, it, expect
- Others: Mocha, QUnit, Jest (Facebook)
- Often used with a mocking library like Sinon
const SuperAwesomeModule = {
featureA: () => {
...
},
featureB: () => {
...
}
}
- test suite begins with "describe"
- takes a string (spec suite title) and a function (block of code being tested)
- suites can be nested
describe('SuperAwesomeModule', () => {
describe('featureA', () => {
});
describe('featureB', () => {
});
});
- call global Jasmine function
it(<string>, <fn>)
- a spec contains one or more expectations
- expectation: an assertion that is either true or false.
- spec with all true expectations: pass
- spec with one or more false expectations: fail
describe('SuperAwesomeModule', () => {
describe('featureA', () => {
it('should calculate some super awesome calculation', () => {
...
});
it('should also do this correctly', () => {
...
});
});
});
- call global Jasmine function
expect(<actual>).<matcher(expectedValue)>
- a matcher implements boolean comparison between the actual value and the expected value
describe('SuperAwesomeModule', () => {
describe('featureA', () => {
it('should calculate some super awesome calculation', () => {
expect(SuperAwesomeModule.featureA([1, 2, 4]).toEqual(7);
});
it('should also do this correctly', () => {
expect(SuperAwesomeModule.featureB('...').toBe(true);
});
});
});
expect(foo).toBe(true); // uses JS strict equality
expect(foo).not.toBe(true);
expect(foo).toEqual(482); // uses deep equality, recursive search through objects
expect(foo).toBeDefined();
expect(foo).not.toBeDefined();
expect(foo).toBeUndefined();
expect(foo).toBeTruthy(); // boolean cast testing
expect(foo).toBeFalsy();
expect(foo).toContain('student'); // find item in array
expect(e).toBeLessThan(pi);
expect(pi).toBeGreaterThan(e);
expect(a).toBeCloseTo(b, 2); // a to be close to b by 2 decimal points
expect(() => {
foo(1, '2')
}).toThrowError();
expect(() => {
foo(1, '2')
}).toThrow(new Error('Invalid parameter type.')
describe('ApiService', function() {
const serviceInTest;
beforeEach(function() {
serviceInTest = new ApiService();
});
afterEach(function() {
...
});
it('retrieves data', function() {
...
});
it('updates data', function() {
...
});
});
describe('SuperAwesomeModule', () => {
xdescribe('featureA', () => {
it('should ...', () => {
});
it('should ...', () => {
});
});
describe('featureB', () => {
xit('should ...', () => {
});
it('should ...', () => {
});
});
});
- test double functions called spies.
- can stub any function and tracks calls to it and all arguments.
- A spy only exists in the describe or it block in which it is defined, and will be removed after each spec.
describe('SuperAwesomeModule', function() {
beforeEach(function() {
// track all calls to SuperAwesomeModule.asyncHelperFunction()
// and return a mock response
spyOn(SuperAwesomeModule, 'asyncHelperFunction').and.returnValue(Promise.resolve(mockData))
});
describe('featureA', function() {
it('should ...', function() {
expect(SuperAwesomeModule.featureA(x)).toBe(y);
// matchers for spies
expect(SuperAwesomeModule.coolHelperFunction).toHaveBeenCalled();
});
});
});
- spec will not start until the done function is called in the call to beforeEach
- spec will not complete until its done is called.
- Default timeout is 5 seconds, can override: jasmine.DEFAULT_TIMEOUT_INTERVAL
describe("long asynchronous specs", function() {
beforeEach(function(done) {
done();
}, 1000);
it("takes a long time", function(done) {
setTimeout(function() {
done();
}, 9000);
}, 10000);
afterEach(function(done) {
done();
}, 1000);
});
We will test drive the implementation of a scoreCalculator
function (sums up scores) that satisfies the following:
should work with one number
should work with more than one score
should treat negative scores as 0
should return zero with empty input
Details
Inside the Angular project, running ng test --single-run --code-coverage
will output something like this:
It's a bit difficult to know which tests exactly ran, so let's configure our terminal spec reporting. To do so, you will need to install the karma-spec-reporter
plugin and configure karma.conf.js
. It should already be included when you ran the initial npm install
.
Tasks:
- in the
plugins
, require thekarma-spec-reporter
:require('karma-spec-reporter')
- in the
reporters
, replace'progress'
with'spec'
- in the
reports
array inside thecoverageIstanbulReporter
object, add'text-summary'
Now, when you run your tests, you should get something like this:
Details
Code: src/app/core/menu
In this module, we will learn the basic steps in setting up unit tests using the Angular testing utilities. There are 3 standard methods of testing Angular components:
- Isolated tests: we treat the component class as vanilla JS. Don't render the component.
- Shallow tests: use the Angular testing utilities to render the component, but don't render children components.
- Integration tests: not end-to-end tests here. In this method we render children components also.
When testing components, we will be using the shallow method of testing components, and when our components take in inputs, and/or we want to test outputs, we will use a test host component.
We first need to import a few of the testing utilities, and also the component to test:
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MenuComponent } from './menu.component';
We start our describe block, and before each of our tests, we want to configure the testing module. In the declarations property is where you declare the component being tested. We first compile the components in test:
let component: MenuComponent;
let fixture: ComponentFixture<MenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MenuComponent ]
})
.compileComponents();
}));
compileComponents()
will ensure that external templates and styles are inlined. This is an async operation, so we use the async
utility, which runs it in a special async test zone. If you're using webpack, this isn't needed, but it's a good idea to always have this here in case your build system changes.
We then get handles on two important pieces:
beforeEach(() => {
fixture = TestBed.createComponent(MenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
- fixture : A fixture for debugging and testing a component. Provides access to the component instance and also the
DebugElement
, a handle on the component's DOM element. - component : The component instance
fixture.detectChanges()
initializes the component (calling ngOnInit()
) and runs the change detection cycle.
With setup out of the way, we can start writing assertions. For instance, a test to ensure that two menu items get rendered:
it('should render two menu items', () => {
const menuItems = fixture.debugElement.queryAll(By.css('a'));
expect(menuItems.length).toBe(2);
});
We use the debugElement
's queryAll
method to retrieve all DebugElements
that satisfy the search, and using the By.css
utitlity.
Running this, you will get an error:
Can't bind to 'routerLink' since it isn't a known property of 'a'
.
Since we aren't importing the module for routing, Angular doesn't recognize this directive. However, we want to shallow test, so we will tell Angular to ignore components and directives not included in the declarations
property by using the NO_ERRORS_SCHEMA
constant:
import { NO_ERRORS_SCHEMA } from '@angular/core';
and declare a new schemas
property when confiuring the test module:
schemas: [NO_ERRORS_SCHEMA]
Write a spec 'should render a different hacker link title'
.
- change the component's
hackerLink
property to something else - trigger a change detection cycle
- Use the
debugElement
and theBy
utiltity to assert that the new title is reflected in the DOM.
hint: Once you obtain the debugElement
reference to the hacker link, you can get the native HTMLElement
through the nativeElement
property.
Details
Code: src/app/status
In this module, we will learn how to test components with inputs and outputs. The best way to test this kind of components is by using a test host component. Essentially, in your test you create a parent component which houses the component you want to test. This way, it's very easy to feed it inputs, and to listen for any output events.
We will be looking at the StatusComponent
, which has the following behavior:
This is how it is used:
<app-status [status]="hacker.status" (newStatus)="updateStatus($event)"></app-status>
It takes in as input a status
which can be 'danger'
, 'safe'
, or 'warning'
. It also exposes a newStatus
event, and whenever fired, it will call the specified function with the new message. If we take a look at the StatusComponent
class and template, the newStatus
event will get emitted when the status component is clicked on.
<div class="status-pulse" (click)="refreshStatus()">
<span class="pulse" [ngClass]="color"></span>
<span class="dot" [ngClass]="color"></span>
</div>
With this knowledge, let's create a test host component:
@Component({
template: '<app-status [status]="appStatus" (newStatus)="updateStatus($event)"></app-status>'
})
class TestHostComponent {
appStatus: string;
updateStatus = jasmine.createSpy('statusSpy');
}
For the TestBed
configuration, we will include both the StatusComponent
and the TestHostComponent
in the declarations. We then obtain a fixture on the test host component, and the test host component instance. Do not call fixture.detectChanges
here since that will trigger the ngOnInit
method.
let testHost: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StatusComponent, TestHostComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
});
With the setup out of the way, we are now ready to write some tests.
Complete the following tests:
should set pulse color to green when input is "safe"
should set pulse color to yellow when input is "warning"
should set pulse color to yellow when input is "warning"
should set pulse color to green when input is undefined
should output a new message when clicked
For the first four tests, you want to follow these steps:
- Arrange: set the
appStatus
property on the test host component to what you are currently testing, so something like'safe'
- Act: trigger a change detection cycle (
fixture.detectChanges()
), and get a reference to the element with class of.pulse
. Use thefixture.debugElement.query()
utility, andBy.css()
. This would look something likefixture.debugElement.query(By.css('.pulse')).nativeElement
- Assert: You can then assert things about the
classList
property of the element.
For the last test:
- Arrange: get a reference to the main container with class
.status-pulse
- Act: simulate a
click()
- Assert: the
testHost.updateStatus
function/spy should have been called. You can also assert things about the argument.
Details
Reference: Test a component with an async service
Code: src/app/hacker-list
In this module, we will learn how to test components with (async) service dependencies. When performing such tests, we must specify the injected services in the providers
property when configuring the testing module:
TestBed.configureTestingModule({
declarations: [ HackerListComponent ],
providers: [
{ provide: ApiService, useValue: mockApiService },
{ provide: Router, useValue: mockRouter }
],
schemas: [NO_ERRORS_SCHEMA]
})
Here we are using the provide
object literal, such that when the DI system retrieves the ApiService
, it will use the provided value. Here we don't provide the real service, but instead a mock service. The mockApiService
should simply be an object that has the same interface as the actual ApiService
:
const mockApiService = {
getHackers: () => { }
};
This component only utilizes the navigate
method of the router, so we can also create a mock for that.
const mockRouter = {
navigate: () => { }
};
At the top of the describe block, in addition to declaring variables for the component
and fixture
,we also want to declare a variable to hold a reference to the injected service:
let component: HackerListComponent;
let fixture: ComponentFixture<HackerListComponent>;
let api: ApiService;
How do we get the injected service? The best way to do so is to get it from the component's injector:
api = fixture.debugElement.injector.get(ApiService);
From here on, we can spy on api
, and not the mockApiService
. It is simply a clone of that object.
Suppose one of your components method performs async work:
ngOnInit() {
this.api.getProducts()
.then((data: any) => {
this.products = data;
});
}
In your test, you should first spy on the service mock and return a controlled response:
spyOn(api, 'getProducts').and.returnValue(Promise.resolve(mockProducts));
Then, there are two methods of testing this:
- use
async
andfixture.whenStable
- use
fakeAsync
andtick
The first is to use the async
testing utility, which is a function that returns a function, which becomes the second argument to the it
call. You must then uses the fixture's whenStable
method which returns a promise when all async work within this test is complete.
it('...', async(() => {
spyOn(api, 'getProducts').and.returnValue(Promise.resolve(mockProducts));
component.ngOnInit();
fixture.whenStable()
.then(() => {
expect(...).toEqual(...);
});
}));
The second method is to use the fakeAsync
testing utility. It allows you to write a test in a more linear fashion:
it('...', fakeAsync(() => {
spyOn(api, 'getProducts').and.returnValue(Promise.resolve(mockProducts));
component.ngOnInit();
flush(); // "flushes" asynchronous tasks
expect(...).toEqual(...);
}));
If you need fine time control, the tick
function simulates the passage of time, and it can take in an optional argument of milliseconds.
Write tests for the initial display
(describe
block)
makes a call to api.getHackers
sets initial data (using async)
: SincengOnInit
performs async work, we use theasync
testing utilitysets initial data (using fakeAsync)'
: usefakeAsync
instead. You will need to use thetick
function here
Write a test for the click on hacker
(describe
block):
should navigate to the hacker/:id path
Relevant imports:
import { async, ComponentFixture, TestBed, fakeAsync, flush } from '@angular/core/testing';
import { HackerListComponent } from './hacker-list.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ApiService } from '../core/services/api.service';
import { Router } from '@angular/router';
import { By } from '@angular/platform-browser';
import { mockHackers } from '../core/helpers.spec';
Details
Code: src/app/core/services
When it comes to testing services in Angular, you could write isolated tests (no Angular testing utilities) or shallow tests (using Angular utilities like the TestBed
and the inject
function). I recommend writing isolated tests for services, as they are essentially just a class, as adding in the Angular helping utilities will probably just add complexity to your tests. If your service depends on other services, you can easily stub them out.
Here is our basic ApiService
:
@Injectable()
export class ApiService {
baseUrl = '/api';
constructor(public http: Http) { }
getHackers() {
return this.http.get(`${this.baseUrl}/hackers`)
.toPromise()
.then((res: Response) => res.json());
}
getHackerDetails(id: string) {
return this.http.get(`${this.baseUrl}/hackers/${id}`)
.toPromise()
.then((res: Response) => res.json());
}
}
When testing services, at the top of your describe block, you will need to declare the a variable that will hold the reference to your service, and create spies for any dependencies.
let service: ApiService;
const httpSpy = jasmine.createSpyObj('http', ['get']);
Using the createSpyObj
method gives us great flexibility as we can instruct it to return different values as needed. Unit tests should isolated, fast, and should not make external http requests, which is why we will stub out the Http
service instead.
Before each spec, create a brand new instance of the service:
beforeEach(() => {
service = new ApiService(httpSpy);
});
Now for each spec, the structure will look like this:
it('...', (done) => {
// create a mock response
// instruct any dependent service to return the mock response
// by using the spy object
// make the call to your service
// if the call is async (returns a Promise), you can listen
// for when the problem resolves, assert, and then call done()
});
Note here that we are using the Jasmine built-in done function. This suffices for our unit tests, and there really is no need to bring in the async
or fakeAsync
utilities. In fact, when dealing with Observables, you will have to use the done
function instead.
Write the following unit tests for both the getHackers
and getHackerDetails
of the ApiService
.
getHackers
:'should return list of hackers'
: You should assert thathttp.get
gets called with'/api/hackers'
, and the data returned is the mock data.getHackerDetails
:'should return hacker details given hacker id'
: You should assert thathttp.get
gets called with'/api/hackers/${id}''
, and the data returned is the mock data.
Details
Code: src/app/core/directives
An attribute directive is used to modify behavior of an existing element or component. Suppose we have a directive that can be added to an input element to prevent numeric input. We can easily achieve this using a @HostListener
and listening for the keydown
event.
import { Directive, HostListener, ElementRef } from '@angular/core';
@Directive({
selector: '[appNonNumeric]'
})
export class NonNumericDirective {
constructor(private element: ElementRef) { }
@HostListener('keydown', ['$event'])
onKeydown(event) {
event.preventDefault();
const numberRegex = /[0-9]/;
if (!numberRegex.test(event.key)) {
this.element.nativeElement.value = event.key;
}
}
}
And its usage:
<input appNonNumeric type="text" placeholder="Search...">
Looking at the implementation, you could very well write an isolated test and test the onKeydown
method. However, we want to test how this directive will make other elements behave. We will be using a test host component along with the Angular testing utitlies.
A test host component can look like this:
@Component({
template: `<input appNonNumeric type="text"/>
<textarea appNonNumeric></textarea>`
})
class TestHostComponent {
}
When testing this, we can use the debugElement
and By
to query for the input. DebugElement
s have a useful triggerEventHandler
that you can call. In this case, we would trigger the keydown
event.
Complete the following tests:
should allow regular text input
: You should query for theinput
element, and trigger thekeydown
event handler. (TODO: detect changes?)should not allow numeric text input for input elements
: Similar setup to the first one, except the event's key property should be a string containing a numbershould allow regular text input for textarea elements
should not allow numeric text input for textarea elements
Details
Code: app/core/pipes
In this module we will learn how to test pipes. Testing pipes in Angular is actually very easy, there is really no set up as we are writing vanilla jasmine tests, without any Angular testing utilities. You should write these kind of isolated tests for both services and pipes.
Suppose we have a pipe to transform any string input to all uppercase letters:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'uppercase'
})
export class UppercasePipe implements PipeTransform {
transform(input: string): any {
return input.toUpperCase();
}
}
To test, we are simply testing a class. Below is the setup and some sample tests:
import { UppercasePipe } from './uppercase.pipe';
describe('UppercasePipe', () => {
let pipe: UppercasePipe;
beforeEach(() => {
pipe = new UppercasePipe();
});
it('creates an instance', () => {
expect(pipe).toBeTruthy();
});
it('transforms input string to uppercase', () => {
expect(pipe.transform('angular rocks!')).toBe('ANGULAR ROCKS!');
});
});
In these exercises, we are going to test-drive the implementation of the ShortDatePipe
, which will transform an input ISO date string and return a "short date" format.
'1960-06-01T11:01:12.720Z' ----> '06/01/1960, 04:01am'
Complete the following tests:
creates an instance
should not throw error
returned value should contain date format dd/mm/yyyy
returned value should contain time hh:mm[am|pm]
should convert ISO string to correct date format (am)
should convert ISO string to correct date format (pm)
You can use this sample data:
'1972-08-23T15:22:34.694Z' ----> '06/01/1960, 04:01am'
'1980-10-04T21:35:51.869Z' ----> '10/04/1980, 02:35pm'
Details
Code: src/app/hacker-detail
Testing routed components is not much different than testing components with async services, the only difference is that instead of dealing with timers or Promises, most likely you'll be dealing with Observables
since the router exposes certain Observable
properties to read information from the current route.
Take for instance the HackerDetailComponent
:
@Component({
selector: 'app-hacker-detail',
templateUrl: './hacker-detail.component.html',
styleUrls: ['./hacker-detail.component.scss']
})
export class HackerDetailComponent implements OnInit {
@Input() id: string;
hacker: Hacker;
constructor(private api: ApiService, private route: ActivatedRoute) { }
ngOnInit() {
this.route.params.subscribe(params => {
this.id = params['id'];
this.renderDetails(this.id);
});
}
renderDetails(id: string) {
this.api.getHackerDetails(id)
.then((data) => {
this.hacker = data;
});
}
}
It has two injected dependencies, the ApiService
and the ActivatedRoute
. You know how to create a mock for the api service (simply return a resolved promise). However, the params
property is an observable that emits an object. Here, we care about the id
param, since our route was declared as hackers/:id
.
At the beginning of the describe block, you can create mocks for both:
const mockApiService = {
getHackerDetails: (id) => Promise.resolve(mockHackers[3])
};
const mockActivatedRoute = {
params: Observable.of({ id: 'f1b2e9bf-2794-4ccf-a869-9ddb93478f70'})
};
Using Observable.of()
is a very convinient way of wrapping objects into an observable.
When configuring the TestBed
, for the providers you instruct Angular to use these when the service dependencies are injected:
providers: [
{ provide: ApiService, useValue: mockApiService },
{ provide: ActivatedRoute, useValue: mockActivatedRoute }
],
In your specs, calling fixture.detectChanges()
will trigger ngOnInit
, which will retrieve the id parameter, and then call renderDetails
, which will then call the getHackerDetails
method on the api
. Lots of async here, so use async()
along with fixture.whenStable()
.
Complete the following tests:
should set the correct hacker name
should set the correct hacker status message
Details
There are specific things that as a developer and tester and you can do to create a better testing workflow. From terminal reporting, to commit hooks, you should take advantage of the tools available.
The Angular CLI generates a project for you with testing included out of the box. It's a good idea to generate code coverage reports when you run your tests:
ng test --single-run --codecoverage
Better yet, create an npm script for this:
"test": "ng test --single-run --code-coverage"
and also a script to watch your tests automatically:
"test:watch": "ng test --code-coverage"
Also, configure terminal reporting (refer to Module 2 above).
There are mixed opinions on whether or not you should enforce coverage thresholds. Sure, a codebase of 99% coverage may not necessarily mean that your code is bug free, but tested code is one major step in the way of producing clean code. Enforcing coverage thresholds will promote testability among your team (specially if your team is new to testing), and you can ensure that untested code is not making its way to your codebase.
Install the karma-istanbul-threshold
module:
npm i karma-istanbul-threshold --save-dev
and add it to the plugins in karma.conf.js
:
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('karma-istanbul-threshold'),
require('@angular/cli/plugins/karma'),
require('karma-spec-reporter')
],
add the 'json'
reporter to the coverageIstanbulReporter
object:
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly', 'json', 'text-summary' ],
fixWebpackSourcePaths: true
},
add the 'istanbul-threshold'
reporter:
reporters: config.angularCli && config.angularCli.codeCoverage
? ['spec', 'coverage-istanbul', 'istanbul-threshold']
: ['spec', 'kjhtml'],
and finally, configure the thresholds:
istanbulThresholdReporter: {
src: 'coverage/coverage-final.json',
reporters: ['text'],
thresholds: {
global: {
statements: 90,
branches: 65,
lines: 90,
functions: 90,
},
each: {
statements: 80,
branches: 60,
lines: 60,
functions: 80,
},
}
},
Now, if any of these stats fall below the specified thresholds, running ng test
will fail, even if each spec is passing:
Husky can be used to easily configure git hooks to prevent bad commits. It's an npm module, so install it:
npm i --save-dev husky
Then, you can configure a precommit
and prepush
hook by simply adding npm scripts:
"precommit": "npm run lint",
"prepush": "ng test --single-run --code-coverage"
Before committing, it will run the linter, and before pushing your branch, it will run the test suite. This combined with coverage thresholds can provide a powerful way of enforcing clean, tested code.
Details
You can use TravisCI to automatically test your code as it's pushed to GitHub, and configure it to run for every pull request.
Head over to TravisCI and sign in with your GitHub account. You can then "flick the repository" switch to "on" for your repo. The next step is to add a .travis.yml file.
dist: trusty
sudo: required
language: node_js
node_js:
- '6'
addons:
apt:
packages:
- google-chrome-beta
env:
- CHROME_CANARY_BIN=/usr/bin/google-chrome-beta
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start &
- sleep 3
install:
- npm install
- npm install -g codecov
script:
- npm test
- codecov -f coverage/coverage-final.json
There is a a lot going on here. We are instructing TravisCI to use Ubuntu Trusty, Node 6.x, and further instructions to in order to get Chrome Canary headless running. In addition, we will be using codecov.io in order to provide coverage reports for us. It works out of the box with TravisCI, simply sign up using your GitHub account.
Once you open a PR or push any branch, it will trigger a TravisCI build:
If the build fails, you will know both on GitHub and on TravisCI:
Once fixed, repush your branch, and the build triggers again:
In addition, since we have enabled reporting with Codecov, we get a codecov bot reporting the coverage: