Skip to content

Instantly share code, notes, and snippets.

@Quramy
Last active September 4, 2020 17:24
Show Gist options
  • Save Quramy/1dd5bed0bce1e7f34b79184453d1790f to your computer and use it in GitHub Desktop.
Save Quramy/1dd5bed0bce1e7f34b79184453d1790f to your computer and use it in GitHub Desktop.
Performance Angular unit testing

Performance of unit testing Angular app

I'm loving Angular, but running unit tests on Karma gets my nerves. It's too slow for me.

In this post, I explain mechanics under Angular's testing module and how to improve the performance.

What makes my tests slow?

To evaluate Angular unit testing performance I captured the CPU profiling with running Karma.

The above profiling was captured under the following condition:

  • Angular version: 4.0.3
  • Scaffold with angular-cli
  • Put 300 components to app module
  • Run 15 specs on Karma and Chrome
/* app.component.spec.ts */

import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        AppModule, // This module has about 300 components.
      ],
    }).compileComponents();
  }));

  for (let x = 0; x < 15; x++) {
    it('should render title in a h1 tag ' + x, async(() => {
      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      const compiled = fixture.debugElement.nativeElement;
      expect(compiled.querySelector('h1').textContent).toContain('app works!');
    }));
  }
});

The above pictures indicates that the execution time of compilation components accounts for over 70%.

The test proceeds as follows:

  1. Start a spec
  2. Configure testingModule
  3. Create component fixture for AppComponent
    1. Compile all components and parse their templates included in AppModule
    2. Create a module factory using the compilation result
    3. Create an AppComponent
  4. Assertion
  5. End the spec
  6. Start the next spec...

The main point is that the compilation all components is executed in every specs.

Imagine bootstrapping your application in JiT compilation mode dozen of times.

Improvement performance

Isolated component tests

Make component unit tests independent of other modules. This technique is called "isolated unit tests".

If you want detail, see https://angular.io/docs/ts/latest/guide/testing.html#!#isolated-component-tests .

Separate your modules

Check the module imported from TestBed is really necessary for your test? The compilation time gets longer as the module gets larger. If the module includes a lot of unused components, you can separate them into another module. And purge the separated module from TestBed.

  • before
                                               +----------------------------+
                                               |          AppModule         |
                                               |----------------------------|
                                               |                            |
SomeComponent(test target)  --- (depends on) --+-> FooComponent  (used)     |
                                               |                            |
                                               |   HogeComponent (unused)   |
                                               |                            | 
                                               |   BarComponent  (unuesd)   |
                                               |                            |
                                               +----------------------------+
  • after
                                               +----------------------------+
                                               |           Module1          |
                                               |----------------------------|
                                               |                            |
SomeComponent(test target)  --- (depends on) --+-> FooComponent  (used)     |
                                               |                            |
                                               +----------------------------+


                                               +----------------------------+
                                               |           Module2          |
                                               |----------------------------|
                                               |                            |
                                               |   HogeComponent (unused)   |
                                               |                            | 
                                               |   BarComponent  (unuesd)   |
                                               |                            |
                                               +----------------------------+

Keep dependencies small !

Configure TestBed's compiler

This technique is so hacky.

In many cases, TestBed has the same configuration for each specs in a test code. For example:

/* MyComponent.component.spec.ts */

describe('MyComponent', () => {
  let fixture: ComponentFixuture<MyComponent>;
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ AppModule ]
    }).compileComponents();
    fixture = TestBed.createComponent(MyComponent);
    fixture.detectChanges();
  }));

  it('should render something when someCondition is true', async(() => {
    fixture.componentInstance.someCondition = true;
    /* assertion something is rendered... */
  }));

  it('should not render something when someCondition is false', async(() => {
    fixture.componentInstance.someCondition = false;
    const compiled = fixture.debugElement.nativeElement;
    /* assertion something is not rendered... */
  }));
});

In the above example, the both specs need AppModule (and MyComponent included it). TestBed creates dynamic testing modules twice and the created testing modules have the same declaration because they depend on the same module and components. In other words, the compiled module factories are reusable in this file.

So if you customize the compiler used by TestBed to be able to turn use the executed compilation results, you can reduce the total compilation time. I've created a helper library to do this.

But don't forget that this method has a risk of destroying the idempotency of the unit testing. If you practice this technique, take the risk into consideration.

@jessycormier
Copy link

Thanks for this investigation; It's very interesting. I'm working in a project that has over 1600 tests and it is taking a long time for them to run. I've been recently looking into ways to write our tests to trim the time down as well as understand what's going on ' under the hood '.

@joshtune
Copy link

joshtune commented Apr 5, 2019

interesting I will keep that in mind

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment