Angular ~ Testing - rohit120582sharma/Documentation GitHub Wiki
Angular provides Unit testing
, Integration testing
and E2E (End-to-End) testing
all of which are very different from each other.
Unit testing works by isolating small "units" of code so that it can be tested from every angle while E2E testing works by testing full areas of the application by running a test through its entire stack of operations against a special HTTP server from the start to the end (hence end to end).
Karma is a tool which lets us spawn browsers that are specified within its configuration file and run jasmine tests inside of them all from the command line. The results of the tests are also displayed on the command line.
A browser can be captured either:
- manually, by visiting the URL where the Karma server is listening (typically http://localhost:9876/)
- or automatically by letting Karma know which browsers to start when Karma is run
Karma also watches all the development files for changes, specified within the configuration file, and whenever any file changes, it re-runs the tests automatically. Each browser then loads the source files inside an IFrame, executes the tests and reports the results back to the server.
Karma is not a testing framework, nor an assertion library. Karma just launches an HTTP server, handles the process of creating HTML files, opening browsers and running tests and returning the results of those tests to the command line.
Jasmine is a testing framework that supports Behavior Driven Development. We write tests in Test Suites which are composed of one or more Test Specs which themselves are composed of one or more Test Expectations.
Main concepts:
- Suites - describe(string, function) functions, take a title and a function containing one or more specs.
- Specs - it(string, function) functions, take a title and a function containing one or more expectations.
- Expectations - are assertions that evaluate to true or false. Basic syntax reads expect(actual).toBe(expected)
- Matchers - are predefined helpers for common assertions. Eg: toBe(expected), toEqual(expected).
The recommended approach is to install Karma (and all the plugins your project needs) locally in the project directory.
You will need to install karma-cli
globally if you want to run Karma on Windows from the command line. Then you can run Karma simply by karma
from anywhere and it will always run the local version.
# Install Karma Commandline Interface:
npm install karma-cli -g
# Install Karma:
npm install karma --save-dev
# Install plugins that your project needs:
npm install karma-jasmine --save-dev
npm install karma-jasmine-html-reporter --save-dev
npm install karma-typescript --save-dev
npm install karma-typescript-angular2-transform --save-dev
npm install karma-chrome-launcher --save-dev
npm install karma-html-reporter --save-dev
npm install karma-coverage --save-dev
# Install Jasmine:
npm install jasmine-core --save-dev
# Install Jasmine type-definition:
npm install @types/jasmine --save-dev
In order to serve you well, Karma needs to know about your project in order to test it and this is done via a configuration file. The easiest way to generate an initial configuration file is by using the below command which will ask some configuration questions and will generate karma.conf.js
.
karma init
Within the configuration file, the configuration code is put together by setting module.exports to point to a function which accepts one argument: the configuration object.
// Karma configuration
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine', 'karma-typescript'],
// list of files / patterns to load in the browser
files: [
// Polyfills
'node_modules/core-js/client/shim.js',
'node_modules/reflect-metadata/Reflect.js',
// zone.js
'node_modules/zone.js/dist/zone.js',
'node_modules/zone.js/dist/long-stack-trace-zone.js',
'node_modules/zone.js/dist/proxy.js',
'node_modules/zone.js/dist/sync-test.js',
'node_modules/zone.js/dist/jasmine-patch.js',
'node_modules/zone.js/dist/async-test.js',
'node_modules/zone.js/dist/fake-async-test.js',
// RxJs
{ pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false },
{ pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false },
// Assets
{ pattern: 'src/app/**/*.+(ts|html)' }
],
// list of files to exclude
exclude: [
'node_modules'
],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'**/*.ts': ['karma-typescript', 'coverage']
},
karmaTypescriptConfig: {
bundlerOptions: {
entrypoints: /\.spec\.ts$/,
transforms: [
require('karma-typescript-angular2-transform')
]
},
compilerOptions: {
lib: ['ES2015', 'DOM']
}
},
// optionally, configure the reporter
coverageReporter: {
type : 'html',
dir : 'coverage/'
},
// test results reporter to use
// possible values: 'dots', 'progress'
// coverage reporter generates the coverage
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'karma-typescript', 'kjhtml', 'coverage'],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity
})
}
To run the test, we need to start Karma test runner. By default, Karma will look for karma.conf.js
file.
karma start
karma start --code-coverage
Unit testing is also called Isolated testing. It’s the practice of testing small isolated pieces of code. If your test uses some external resource, like the network or a database, it’s not a unit test.
Sometimes in order to test a feature, we need to perform some setup. These activities are called setup and teardown (for cleaning up) and Jasmine has a few functions we can use to make this easier:
-
beforeAll - This function is called once, before all the specs in
describe
test suite are run. - afterAll - This function is called once after all the specs in a test suite are finished.
-
beforeEach - This function is called before each test specification,
it
function, has been run. - afterEach - This function is called after each test specification has been run.
Testing with real instances of dependencies causes our test code to know about the inner workings of other classes resulting in tight coupling and brittle code.
The goal is to test pieces of code in isolation without needing to know about the inner workings of their dependencies.
We do this by creating Mocks; we can create Mocks using fake classes, extending existing classes or by using real instances of classes but taking control of them with Spys.
There are two ways to create a spy in Jasmine:
-
spyOn()
- A Spy is a feature of Jasmine which lets you take an existing class, function, object and mock it in such a way that you can control what gets returned from functions.
- It can only be used when the method already exists on the object
-
createSpy()
- It will return a brand new function
/*auth.service.ts*/
export class AuthService {
isAuthenticated(): boolean {
return !!localStorage.getItem('token');
}
}
/*login.component.ts*/
import {Component} from '@angular/core';
import {AuthService} from "./auth.service";
@Component({
selector: 'app-login',
template: `
<a>
<span *ngIf="needsLogin()">Login</span>
<span *ngIf="!needsLogin()">Logout</span>
</a>
`
})
export class LoginComponent {
constructor(private auth: AuthService) {
}
needsLogin() {
return !this.auth.isAuthenticated();
}
}
/*login.component.spec.ts*/
import {LoginComponent} from './login.component';
import {AuthService} from "./auth.service";
describe('Component: Login', () => {
let component: LoginComponent;
let service: AuthService;
let spy: any;
beforeEach(()=>{
service = new AuthService();
component = new LoginComponent(service);
});
afterEach(()=>{
service = null;
component = null;
});
it('canLogin returns false when the user is not authenticated', ()=>{
spy = spyOn(service, 'isAuthenticated').and.returnValue(false);
expect(component.needsLogin()).toBeTruthy();
expect(spy).toHaveBeenCalled();
});
it('canLogin returns false when the user is not authenticated', ()=>{
spy = spyOn(service, 'isAuthenticated').and.returnValue(true);
expect(component.needsLogin()).toBeFalsy();
expect(spy).toHaveBeenCalled();
});
});
The Angular Test Bed (ATB) is a higher level Angular Only testing framework that allows us to easily test behaviours that depend on the Angular Framework.
- It allows us to test the interaction of a directive or component with it’s template.
- It allows us to easily test change detection.
- It allows us to test and use Angulars DI framework.
- It allows us to test user interaction via clicks & input fields.
- By using the ATB and fixtures we can inspect the components view through
fixture.debugElement
and also trigger a change detection run by callingfixture.detectChanges()
.
We configure a testing module using the TestBed
class. This creates a test Angular Module which we can use to instantiate components, perform dependency injection and so on.
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { LoginComponent } from './login.component';
import { AuthService } from './auth.service';
describe('Component: Login', () => {
let environment:any;
let component:LoginComponent;
let fixture:ComponentFixture<LoginComponent>;
let authService:AuthService;
let el: DebugElement;
beforeAll(()=>{
environment = TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
});
beforeEach(() => {
environment
.configureTestingModule({
declarations: [LoginComponent],
providers: [AuthService]
});
});
});
- Once the ATB is setup we can then use it to instantiate components and resolve dependencies.
- A
fixture
is a wrapper for a component and it’s template. - We create an instance of a component fixture through the TestBed, this injects the AuthService into the component constructor.
- We can find the actual component from the
componentInstance
on thefixture
. - We can get resolve dependencies using the TestBed injector by using the
get
function.
beforeEach(() => {
// create component and test fixture
fixture = TestBed.createComponent(LoginComponent);
// get test component from the fixture
component = fixture.componentInstance;
// UserService provided to the TestBed
authService = TestBed.get(AuthService);
});
- By using the ATB and
fixtures
we can inspect the components view throughfixture.debugElement
and also trigger a change detection run by callingfixture.detectChanges()
in order to update the view. - A DebugElement is a wrapper to the low level DOM element that represents the components view, via the
fixture.debugElement
property. - We can get references to other child nodes by querying this
debugElement
with aBy
class.
beforeEach(() => {
// get the "a" element by CSS selector (e.g., by class name)
el = fixture.debugElement.query(By.css('a'));
});
it('login button hidden when the user is authenticated', () => {
// To being with Angular has not done any change detection so the content is blank.
expect(el.nativeElement.textContent.trim()).toBe('');
// Once we trigger a change detection run; Angular checks property bindings and update view.
// And assert with updated view
fixture.detectChanges();
expect(el.nativeElement.textContent.trim()).toBe('Login');
// Change the authetication state to true
// The label is still Login!
spyOn(authService, 'isAuthenticated').and.returnValue(true);
expect(el.nativeElement.textContent.trim()).toBe('Login');
// We need changeDetection to run and for angular to update the template.
// Now the label is Logout
fixture.detectChanges();
expect(el.nativeElement.textContent.trim()).toBe('Logout');
});
There are a few ways we can handle asynchronous code in our tests, one is the Jasmine way and two are Angular specific.
- The jasmine
done
function and spy callbacks. We attach specific callbacks to spies so we know when promises are resolves, we add our test code to those callbacks and then we call thedone
function. This works but means we need to know about all the promises in our application and be able to hook into them.
- We can use the Angular
async
andwhenStable
functions, we don’t need to track the promises ourselves but we still need to lay our code out via callback functions which can be hard to read. - We wrap our test spec function in another function called
async
. - This
async
function executes the code inside it’s body in a special async test zone. This intercepts and keeps track of all promises created in it’s body. - Only when all of those pending promises have been resolved does it then resolves the promise returned from
whenStable
.
import {TestBed, ComponentFixture, async, whenStable, fakeAsync, tick} from '@angular/core/testing';
...
it('Button label via async() and whenStable()', async(() => {
fixture.detectChanges();
expect(el.nativeElement.textContent.trim()).toBe('Login');
spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(el.nativeElement.textContent.trim()).toBe('Logout');
});
component.ngOnInit();
}));
- We can use the Angular
fakeAsync
andtick
functions, this additionally lets us lay out our async test code as if it were synchronous. - Like
async
we wrap the test spec function in a function calledfakeAsync
. ThefakeAsync
function executes the code inside it’s body in a special fake async test zone. - We call
tick()
when there are pending asynchronous activities we want to complete. Thetick()
function blocks execution and simulates the passage of time until all pending asynchronous activities complete. -
fakeAsync
does have some drawbacks, it doesn’t track XHR requests for instance.
import {TestBed, ComponentFixture, async, whenStable, fakeAsync, tick} from '@angular/core/testing';
...
it('Button label via fakeAsync() and tick()', fakeAsync(() => {
expect(el.nativeElement.textContent.trim()).toBe('');
fixture.detectChanges();
expect(el.nativeElement.textContent.trim()).toBe('Login');
spyOn(authService, 'isAuthenticated').and.returnValue(Promise.resolve(true));
component.ngOnInit();
tick();
fixture.detectChanges();
expect(el.nativeElement.textContent.trim()).toBe('Logout');
}));