Angular 2 Testing

Unit Testing is an integral part of any project because it allows for better code structuring and splitting it into small, testable modules and also adding new features in a more secure way and to achieve this we recommend the Angular 2 Karma solution.

In my previous tutorial I’ve built an Angular 2 Seed project and we will use it for Unit Testing using Karma and Jasmine.

 

1. Angular 2 Karma Project Structure and Dependencies

We’ll be needing new directories within our project so we’ll restructure it as follows:

 

|-- src
|    |-- app
|    |    |-- components
|    |    |-- models
|    |    |-- services
|    |    |-- main.ts
|    |-- assets
|    |    |-- fonts
|    |    |-- img
|    |-- test // Here we'll write our unit tests
|    |-- index.html
|-- public

 

Our project needs a bunch of new libraries so we’ll add them in our package.json file:

 

"jasmine": "2.3.2",
"karma": "^0.13.22",
"karma-chrome-launcher": "^0.2.1",
"karma-cli": "^0.0.4",
"karma-htmlfile-reporter": "^0.2.2",
"karma-jasmine": "^0.3.6",
"rimraf": "^2.4.3"

 

2. Initializing

With Angular 2 Karma needs an initialisation file which differentiates your app files from your test files and other useful information such as DEFAULT_TIMEOUT_INTERVAL which sets how much time Karma needs to wait for an asynchronous task before throwing a timeout error. The file karma-test-shim.js will be added in our root directory:

 

// Turn on full stack traces in errors to help debugging
// Error.stackTraceLimit = Infinity;
Error.stackTraceLimit = 0;


jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;

// // Cancel Karma's synchronous start,
// // we will call `__karma__.start()` later, once all the specs are loaded.
__karma__.loaded = function() {};


System.config({
  packages: {
    'base/public/app': {
      defaultExtension: false,
      format: 'register',
      map: Object.keys(window.__karma__.files).
            filter(onlyAppFiles).
            reduce(function createPathRecords(pathsMapping, appPath) {
              var moduleName = appPath.replace(/^\/base\/public\/app\//, './').replace(/\.js$/, '');
              pathsMapping[moduleName] = appPath + '?' + window.__karma__.files[appPath]
              return pathsMapping;
            }, {})

      }
    }
});

System.import('angular2/testing').then(function(testing) {
  return System.import('angular2/platform/testing/browser').then(function(providers) {
    testing.setBaseTestProviders(providers.TEST_BROWSER_PLATFORM_PROVIDERS,
                                 providers.TEST_BROWSER_APPLICATION_PROVIDERS);
  });
}).then(function() {
  return Promise.all(
    Object.keys(window.__karma__.files) // All files served by Karma.
    .filter(onlySpecFiles)
    // .map(filePath2moduleName)        // Normalize paths to module names.
    .map(function(moduleName) {
      // loads all spec files via their global module names (e.g. 'base/public/app/hero.service.spec')
      return System.import(moduleName);
    }));
})
.then(function() {
  __karma__.start();
}, function(error) {
  __karma__.error(error.stack || error);
});


function filePath2moduleName(filePath) {
  return filePath.
           replace(/^\//, '').              // remove / prefix
           replace(/\.\w+$/, '');           // remove suffix
}


function onlyAppFiles(filePath) {
  return /^\/base\/public\/app\/.*\.js$/.test(filePath)
}


function onlySpecFiles(path) {
  return /^\/base\/public\/test\/.*\.js$/.test(path);
}

 

3. Configuration

Angular 2 Karma also needs a configuration file which sets which files should be loaded and what browsers should be used for testing. This new file called karma.conf.js will be added to our root directory:

 

module.exports = function(config) {
  config.set({

    basePath: '',

    frameworks: ['jasmine'],

    files: [
      // paths loaded by Karma
      {pattern: 'node_modules/systemjs/dist/system-polyfills.js', included: true, watched: true},
      {pattern: 'node_modules/systemjs/dist/system.src.js', included: true, watched: true},
      {pattern: 'node_modules/es6-shim/es6-shim.js', included: true, watched: true},
      {pattern: 'node_modules/angular2/bundles/angular2-polyfills.js', included: true, watched: true},
      {pattern: 'node_modules/rxjs/bundles/Rx.js', included: true, watched: true},
      {pattern: 'node_modules/angular2/bundles/angular2.js', included: true, watched: true},
      {pattern: 'node_modules/angular2/bundles/testing.dev.js', included: true, watched: true},
      {pattern: 'node_modules/angular2/bundles/router.dev.js', included: true, watched: true},
      {pattern: 'node_modules/angular2/bundles/http.dev.js', included: true, watched: true},

      {pattern: 'karma-test-shim.js', included: true, watched: true},

      // paths loaded via module imports
      {pattern: 'public/**/*.js', included: false, watched: true},

      // paths loaded via Angular's component compiler
      // (these paths need to be rewritten, see proxies section)
      {pattern: 'public/**/*.html', included: false, watched: true},
      {pattern: 'public/**/*.css', included: false, watched: true},

      // paths to support debugging with source maps in dev tools
      {pattern: 'src/**/*.ts', included: false, watched: false},
      {pattern: 'public/**/*.js.map', included: false, watched: false}
    ],

    // proxied base paths
    proxies: {
      // required for component assests fetched by Angular's compiler
      "/app/": "/base/public/app/"
    },

    htmlReporter: {
      outputFile: 'report/index.html'
    },

    reporters: ['progress', 'html'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: true
  })
}

 

4. Improving NPM Commands

We’ll add a bunch of new commands to our package.json file for Angular 2 Karma which will allow to automatically build and run the tests, clean our project and open generated testing report:

 

"scripts": {
    "start": "concurrently \"tsc --watch\" \"gulp watch\" \"npm run serve\" ",
    "clean": "rimraf public/*",
    "build": "tsc && gulp --production",
    "serve": "lite-server --port=8000",
    "test": "tsc && (karma start karma.conf.js; npm run report)",
    "report": "lite-server -c bs-config.report.json",
    "postinstall": "typings install --ambient"
}

 

  • npm start concurrently starts the typescript in watch mode, the gulp watch process and serves our application
  • npm run clean will delete all files from public directory
  • npm run build builds our application for production
  • npm run serve will open our application into a new browser instance
  • npm run test compiles typescript files, runs unit tests and then open the generated report
  • npm run report will open the last generated testing report into a new browser instance
  • npm run postinstall will install the typings files for typescript

 

5. Writing Unit Tests

All of our unit tests will be placed under the src/test directory and in the following examples we’ll not just test the client because we’ll set them up in order to place real requests to our Laravel API, so the entire application including the server side will be tested.

We’ll start with a basic test for Angular 2 karma of Jasmine’s matchers:

 

describe('Sanity Test', () => {

	it('Should test matchers', () => {

		let _undefined, _defined = true;
		
		expect("a" + "b").toBe("ab");

		expect(_undefined).toBeUndefined();
		expect(_defined).toBeDefined();

		expect(!_defined).toBeFalsy();
		expect(_defined).toBeTruthy();
		expect(null).toBeNull();

		expect(1 + 1).toEqual(2);
		expect(5).toBeGreaterThan(4);
    	expect(5).toBeLessThan(6);

		expect("abcdbca").toContain("bcd");
		expect([4, 5, 6]).toContain(5);
		expect("abcdefgh").toMatch(/efg/);

		expect("abcdbca").not.toContain("xyz");
		expect("abcdefgh").not.toMatch(/123/);
	});
});

 

6. Testing Application Routes

Next we’ll move onto something more complex such as testing application routes. Before each test we’ll have to provide all modules required by the Angular Routing system using beforeEachProviders method and also inject Location and Router using beforeEach:

 

import {it, describe, expect, inject, injectAsync, beforeEach, beforeEachProviders} from 'angular2/testing';

import {provide} from 'angular2/core';
import {RouteRegistry, Router, ROUTER_PRIMARY_COMPONENT, Location} from 'angular2/router';
import {RootRouter} from 'angular2/src/router/router';

import {AppComponent} from '../app/components/app';

describe('Routing', () => {

    let location: Location;
    let router: Router;

    beforeEachProviders(() => [ 
        RouteRegistry,
        Location,
        provide(Router, {useClass: RootRouter}),
        provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppComponent})
    ]);

    beforeEach(inject([Router, Location], (r, l) => {
        router = r;
        location = l;
    }));

    it('Should navigate to Login', (done) => {   
        router.navigate(['Login']).then(() => {
            expect(location.path()).toBe('/login');
            done();
        }).catch(e => done.fail(e));
    });

    it('Should navigate to Register', (done) => {   
        router.navigate(['Register']).then(() => {
            expect(location.path()).toBe('/register');
            done();
        }).catch(e => done.fail(e));
    });
});

 

7. Test the Login Process

In the next example we’ll test the login process. Beside modules provided in the above example we’ll also have to provide modules for HTTP Services and User Model:

 

import {it, describe, expect, inject, injectAsync, beforeEach, beforeEachProviders} from 'angular2/testing';

import {provide} from 'angular2/core';
import {HTTP_PROVIDERS, Http}   from 'angular2/http';
import {RouteRegistry, Router, ROUTER_PRIMARY_COMPONENT, Location} from 'angular2/router';
import {RootRouter} from 'angular2/src/router/router';

import 'rxjs/Rx';

import {HttpService} from '../../../app/services/http';
import {User} from '../../../app/models/user';
import {AppComponent} from '../../../app/components/app';

describe('User Login', () => {

    beforeEachProviders(() => [ 
        Http,
        HTTP_PROVIDERS, 

        RouteRegistry,
        Location,
        provide(Router, {useClass: RootRouter}),
        provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppComponent}),

        User, 
        HttpService,
    ]);

    it('Should set auth token', inject([User], (user:User) => {
        let token = "h83hdks95gt7";
        user.setAuthToken(token);

        expect(user.getAuthToken()).toBe(token);
    }));

    it('Should require email', injectAsync([User], (user:User) => {
        return user.login()
                   .then((data:any) => {
                       expect("Login").toContain("errors");
                   })
                   .catch((errors) => {
                       expect(errors.has("requiredEmail")).toBeTruthy();
                   });
    }));

    it('Should require password', injectAsync([User], (user:User) => {
        return user.login()
                   .then((data:any) => {
                       expect("Login").toContain("errors");
                   })
                   .catch((errors) => {
                       expect(errors.has("requiredPassword")).toBeTruthy();
                   });
    }));

    it('Should require valid email', injectAsync([User], (user:User) => {
        user.email = "invalid_email";

        return user.login()
                   .then((data:any) => {
                        expect("Login").toContain("errors");
                   })
                   .catch((errors) => {
                       expect(errors.has("invalidEmail")).toBeTruthy();
                   });
    }));

    it('Should require valid credentials', injectAsync([User], (user:User) => {
        user.email    = Math.random().toString() + "@email.com";
        user.password = "123456";

        return user.login()
                   .then((data:any) => {
                       expect("Login").toContain("errors");
                   })
                   .catch((errors) => {
                       expect(errors.has("invalidCredentials")).toBeTruthy();
                   });
    }));

    it('Should successfully login', injectAsync([User], (user:User) => {
        user.email    = "some_valid_email(at)gmail.com";
        user.password = "a_valid_password";
        
        return user.login()
                   .then((data:any) => {
                       expect(data.token).toBeDefined();
                   })
                   .catch((errors) => {
                       expect("Login").toBe("successful");
                   });
    }));
});

 

8. Run Tests and Check the Report

All we have to do now is run the npm run test command and the tests will be automatically compiled and ran. After this process an html report will be created and opened into your browser:

Angular 2 Karma Testing Report

Our repository for this project is available at https://github.com/agvision/Angular_Seed_Karma.

You can read more Angular 2 and Laravel tutorials on our blog.

 

4 Comments

  1. sireesha vurrinkala

    Hi, this is a very useful article.It helped me alot while writing test cases for routing in angular2 using karma jasmine.but It is showing some error while i am using it in final version of angular2.so can you please provide a basic testcases for routing in final version/master version of angular2.

    the error is regarding router.navigate
    error:cannot read property navigate of undefined

  2. VDN Prasad

    Very nice article ….!

  3. Alin Ghinoiu

    Hey, Shawn!

    Thank you for the compliments. Your issue can arise from a number of issues so please make sure you haven’t missed any steps while setting up testing.

  4. Shawn Broadhead

    I’m am having a heck of a time finding useful information regarding testing Angular 2 with Jasmine. Your project and explanation came close, but when I download the code from the repository and type “npm run test” I get an error that the karma.conf.js file does not exist. I’ve verified that the file exists in the root of the project, but I cannot figure out how to get the test to work. Have you run into this? Do you know how to work around it?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>