Aakash Goplani
Aakash Goplani's Blog

Aakash Goplani's Blog

Simple configuration hacks to improve unit test case performance using Jasmine and Karma in Angular

Aakash Goplani's photo
Aakash Goplani
·Jan 9, 2022·

4 min read

Play this article

Table of contents

  • Make use of Headless Chrome
  • Execute test cases in parallel
  • Style Cleanup & Reseting Testing Module
  • Mock Data Correctly
  • Best Practice

Make use of Headless Chrome

Headless Chrome is a way to run the Chrome browser in a headless environment without the full browser UI. One of the benefits of using Headless Chrome (as opposed to testing directly in Node) is that your JavaScript tests will be executed in the same environment as users of your site. Headless Chrome gives you a real browser context without the memory overhead of running a full version of Chrome.

Configuration in karma.conf.js

browsers: ['Chrome', 'ChromeHeadlessNoSandbox']
customLaunchers: {
  ChromeHeadlessNoSandbox: {
     base: 'ChromeHeadless',
     flags: [
       '--no-sandbox',
       '--disable-gpu',
       '--remote-debugging-port=9222',
       '--disable-site-isolation-trials',
     ]
  }
}

More reading on this configuration from developers.google.com

Configurations in package.json file:

{
  "test-fast": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng test --browsers=ChromeHeadlessNoSandbox --watch=true --codeCoverage=true --source-map=false"
}

npm run test-fast


Execute test cases in parallel

We can utilize karma-parallel plugin to execute test cases parallely. This npm package splits your unit tests into multiple suites that run in parallel with each other, on different threads of your processor. It's highly customizable straight from your karma config file with how many threads you want to use, and how it splits your tests up.

Configuration details:

  1. Install package as dev dependency npm i karma-parallel --save-dev
  2. Changes in karma.conf.js
    frameworks: ['parallel', 'jasmine', ...others] // <- 'parallel' should be first one here
    plugin: [
      ... others,
      require: ('karma-parallel')
    ],
    parallelOptions: {
      executors: require('os') ? Math.ceil(require('os').cpus().length / 2) : 1
      shardStrategy: 'round-robin'
    }
    

NOTE:

  • Though this plugin is awesome, the only problem that I have faced with this is inconsistency in calculation code coverage.
  • I used this plugin until Angular v11. Starting v12, Angular has made many performance boost to Testing Module and since then, I did not felt use to include this!

Style Cleanup & Reseting Testing Module

If you adjust your karma config to run without a headless browser, and using the inspector, inspect your <head> tag, you will notice hundreds if not thousands of <style> tags appended to your body. One more point, after running every test case angular recompiles our test bed configuration, meaning we spent 70% of total time in compilation rather than actual execution of test case. To overcome that we need to override default test bed rest module.

Credits

NOTE:

  • This section make sense till Angular v11.
  • Post v12 Angular has made many performance boost to Testing Module and we can use (in test.ts file):
     getTestBed().initTestEnvironment(
       BrowserDynamicTestingModule(),
       { teardown: { destroyAfterEach: true } } // <- this is the new entry
     );
    
  • This is default in v13 and onwards to no need to make any changes in test.ts, you reap this feature by default (unless you choose to opt-out during upgrade)

Create new file override-reset-test-module-jasmine.ts:

import { getTestBed, TestBed, ComponentFixture } from '@angular/core/testing';

export function overrideResetTestModule() {
  const testBedApi: any = getTestBed();
  const originReset TestBed.resetTestingModule;

  beforeAll(() => {
    TestBed.resetTestingModule();
    TestBed.resetTestingModule = () => TestBed;
  });

  afterEach(() => {
    testBedApi._activeFixtures.forEach((fixture: ComponentFixture<any>) => fixture.destroy());
    testBedApi._instantiated = false;
    cleanStylesFromDOM();
  });

  afterAll(() => {
    TestBed.resetTestingModule = originReset;
    TestBed.resetTestingModule();
  });

  function cleanStylesfromDOM(): void {
    const head: HTMLHeadElement = document.getElementsByTagName('head')[0];
    const styles: HTMLCollectionOf<HTMLStyleElement> I [] = document.getElementsByTagName('style');
    for (let i = 0; i < styles.length; i++) {
      head.removeChild(styles[i]); 
    }
  }

In you spec file, import this helper file:

describe('', => {
  overrideResetTestModule();
  // rest of code
});

Mock Data Correctly

  • Declare components instead of importing modules
  • Always mock data. Especially if your service is interacting with HTTP or Router.
  • This is how I prefer to mock http and router
  • This is very good and detailed article that explains more on importance of mocking and how to mock data

Best Practice


I follow all these practices on my medium sized project in Angular v13 having around 1500 test cases and execution time is less than a minute (~50 seconds)! Yes less than a minute!!

Did you find this article valuable?

Support Aakash Goplani by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this