Angular ~ Testing - rohit120582sharma/Documentation GitHub Wiki

Introduction

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

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

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).

References

Jasmine Matchers


Set up a testing environment

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.

Packages

# 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

Configuration

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
  })
}

Run

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

Testing

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.

Setup and teardown

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 Mocks & Spies

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();
	});
});

Angular Test Bed

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 calling fixture.detectChanges().

Configuring

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]
			});
	});
});

Fixtures and DI

  • 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 the fixture.
  • 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);
});

Change Detection

  • By using the ATB and fixtures we can inspect the components view through fixture.debugElement and also trigger a change detection run by calling fixture.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 a By 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');
});

Testing Asynchronous Code

There are a few ways we can handle asynchronous code in our tests, one is the Jasmine way and two are Angular specific.

Jasmines done function

  • 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 the done function. This works but means we need to know about all the promises in our application and be able to hook into them.

async and whenStable

  • We can use the Angular async and whenStable 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();
}));

fakeAsync and tick

  • We can use the Angular fakeAsync and tick 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 called fakeAsync. The fakeAsync 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. The tick() 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');
}));
⚠️ **GitHub.com Fallback** ⚠️