Angular Test Tips

2017-10-09

Run quality checks before Git commit and/or push

So you started writing unit tests for your Angular app? Unit tests are a good way to verify the basic functionalities in your code and help to verify that introduced changes do not break existing functionalities. A very useful npm package is Husky. It enables you to add Git hooks such as pre-commit, pre-push and pre-merge as as npm commands. You can install it in your project with npm i -D husky or using Yarn if you prefer it: yarn add husky. Then you can use it by adding the appropriate command to package.json. For example, to check the commit message format and linting errors before each commit you can add the commitmsg command like so:

  "scripts": {
    "commitmsg": "conventional-changelog-lint -e && npm run lint:styles && npm run lint"
  }

If you run your tests as Git hooks you trade a small slowdown before commit/merge for early error detection. Which is beneficial because to catch errors early speeds up integration of higher quality code.

The official Angular docs about testing are pretty elaborate and might seem overwhelming for an beginning Angular developer. You might also experience your tests running slow. Let’s see what we can do about that and how to integrate testing as part of your DevOps.

Configuring Karma

If you get started today I highly recommend using the angular-cli to scaffold your project. It creates a Karma config file ready to be used. Basically you only need to add *.spec.ts for each regular implementation file and run ng test. Angular CLI uses Webpack internally and you can always expose it using the -eject flag if you need Webpack customizations that cannot be regularly done through thr angular-cli config.

If you setup running ng test as a Git hook you can add a coverage threshold to reject commits that don’t comply with a coverage percentage.

The default Karma configuration uses PhantomJS as a headless browser. It is quite old though and might run slowly. Luckily, since Chrome 59 there’s a Headless Chrome launcher available that can be used by changing your karma.config.js like this:

  1. Add the Chrome launcher plugin in the plugins section:
plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-mocha-reporter'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('@angular/cli/plugins/karma')
    ]
  1. Use it by adding it to the browsers section:
  browsers: ChromeHeadless

You can also modify the test command in your package.json. My personal setup for Angular CLI looks like this: "test": "ng test --single-run --browsers ChromeHeadless --reporters mocha --sourcemaps=false"

As an alternative you can also consider using Jest, a test runner regularly used in the ReactJS world. Since it does not run a browser at all but …dom it might be even faster. See Testing Angular Faster with Jest

Speeding up Angular TestBed

Another quirk of the Angular TestBed is that it resets on each tests, slowing down execution. This makes sure your tests run in a ‘clean’ environment. In most cases though you don’t need to reset it anyway because you don’t need a clean state. Especially if your components are written in a pure way (without side effects or internal state), or relying only on Input and Output parameters for state (a so called Dumb Component).

This Github discussion has a nice workaround to significantly speed up your tests.

First, create a test.common.spec.ts file in the root of your project (next to the regular test.ts generated by Angular CLI):

  import { TestBed, async, TestModuleMetadata } from '@angular/core/testing';

  const resetTestingModule = TestBed.resetTestingModule,
    preventAngularFromResetting = () => TestBed.resetTestingModule = () => TestBed;
  let allowAngularToReset = () => TestBed.resetTestingModule = resetTestingModule;

  export const setUpTestBed = (moduleDef: TestModuleMetadata) => {
    beforeAll(done => (async () => {
      resetTestingModule();
      preventAngularFromResetting();
      TestBed.configureTestingModule(moduleDef);
      await TestBed.compileComponents();

      // prevent Angular from resetting testing module
      TestBed.resetTestingModule = () => TestBed;
    })().then(done).catch(done.fail));

    afterAll(() => allowAngularToReset());
  };

Normally, in your tests, you would initialize the TestBed like this:

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ BrowserModule, FormsModule, ReactiveFormsModule, SharedModule ],
      providers: [ AuthHttp, AuthService, LoaderService ],
      declarations: [ MyExampleComponent, AnotherComponent ],
      schemas: [ NO_ERRORS_SCHEMA ]
    });
  }));

Let’s change this to use the new setUpTestBed function. First, import the dependencies:

  import { TestModuleMetadata } from '@angular/core/testing';
  import { setUpTestBed } from '../../../test.common.spec';

And changes the test module configuration to:

  let moduleDef: TestModuleMetadata = {
    imports: [ BrowserModule, FormsModule, ReactiveFormsModule, SharedModule ],
    providers: [ AuthHttp, AuthService, LoaderService ],
    declarations: [ MyExampleComponent, AnotherComponent ],
    schemas: [ NO_ERRORS_SCHEMA ]
  };
  setUpTestBed(moduleDef);

In my project, this resulted in a significant decrease in execution time:

Before:

Finished in 46.897 secs / 46.53 secs @ 09:45:52 GMT+0200 (CEST)

SUMMARY:
 299 tests completed

After:

Finished in 17.942 secs / 11.279 secs @ 11:31:21 GMT+0200 (CEST)

SUMMARY:
 301 tests completed

Other considerations and tips: