Angular 4 Unit Tests (TestBed) extremely slow
Asked Answered
C

9

27

I have some unit tests using Angular TestBed. Even if the tests are very simple, they run extremely slow (on avarage 1 test assetion per second).
Even after re-reading Angular documentation, I could not find the reason of such a bad perfomance.

Isolated tests, not using TestBed, run in a fraction of second.

UnitTest

import { Component } from "@angular/core";
import { ComponentFixture, TestBed, async } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { DebugElement } from "@angular/core";
import { DynamicFormDropdownComponent } from "./dynamicFormDropdown.component";
import { NgbModule } from "@ng-bootstrap/ng-bootstrap";
import { FormsModule } from "@angular/forms";
import { DropdownQuestion } from "../../element/question/questionDropdown";
import { TranslateService } from "@ngx-translate/core";
import { TranslatePipeMock } from "../../../../tests-container/translate-pipe-mock";

describe("Component: dynamic drop down", () => {

    let component: DynamicFormDropdownComponent;
    let fixture: ComponentFixture<DynamicFormDropdownComponent>;
    let expectedInputQuestion: DropdownQuestion;
    const emptySelectedObj = { key: "", value: ""};

    const expectedOptions = {
        key: "testDropDown",
        value: "",
        label: "testLabel",
        disabled: false,
        selectedObj: { key: "", value: ""},
        options: [
            { key: "key_1", value: "value_1" },
            { key: "key_2", value: "value_2" },
            { key: "key_3", value: "value_3" },
        ],
    };

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [NgbModule.forRoot(), FormsModule],
            declarations: [DynamicFormDropdownComponent, TranslatePipeMock],
            providers: [TranslateService],
        })
            .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(DynamicFormDropdownComponent);

        component = fixture.componentInstance;

        expectedInputQuestion = new DropdownQuestion(expectedOptions);
        component.question = expectedInputQuestion;
    });

    it("should have a defined component", () => {
        expect(component).toBeDefined();
    });

    it("Must have options collapsed by default", () => {
        expect(component.optionsOpen).toBeFalsy();
    });

    it("Must toggle the optionsOpen variable calling openChange() method", () => {
        component.optionsOpen = false;
        expect(component.optionsOpen).toBeFalsy();
        component.openChange();
        expect(component.optionsOpen).toBeTruthy();
    });

    it("Must have options available once initialized", () => {
        expect(component.question.options.length).toEqual(expectedInputQuestion.options.length);
    });

    it("On option button click, the relative value must be set", () => {
        spyOn(component, "propagateChange");

        const expectedItem = expectedInputQuestion.options[0];
        fixture.detectChanges();
        const actionButtons = fixture.debugElement.queryAll(By.css(".dropdown-item"));
        actionButtons[0].nativeElement.click();
        expect(component.question.selectedObj).toEqual(expectedItem);
        expect(component.propagateChange).toHaveBeenCalledWith(expectedItem.key);
    });

    it("writeValue should set the selectedObj once called (pass string)", () => {
        expect(component.question.selectedObj).toEqual(emptySelectedObj);
        const expectedItem = component.question.options[0];
        component.writeValue(expectedItem.key);
        expect(component.question.selectedObj).toEqual(expectedItem);
    });

    it("writeValue should set the selectedObj once called (pass object)", () => {
        expect(component.question.selectedObj).toEqual(emptySelectedObj);
        const expectedItem = component.question.options[0];
        component.writeValue(expectedItem);
        expect(component.question.selectedObj).toEqual(expectedItem);
    });
});

Target Component (with template)

import { Component, Input, OnInit, ViewChild, ElementRef, forwardRef } from "@angular/core";
import { FormGroup, ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { DropdownQuestion } from "../../element/question/questionDropdown";

@Component({
    selector: "df-dropdown",
    templateUrl: "./dynamicFormDropdown.component.html",
    styleUrls: ["./dynamicFormDropdown.styles.scss"],
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => DynamicFormDropdownComponent),
            multi: true,
        },
    ],
})
export class DynamicFormDropdownComponent implements ControlValueAccessor {
    @Input()
    public question: DropdownQuestion;

    public optionsOpen: boolean = false;

    public selectItem(key: string, value: string): void {
        this.question.selectedObj = { key, value };
        this.propagateChange(this.question.selectedObj.key);
    }

    public writeValue(object: any): void {
        if (object) {
            if (typeof object === "string") {
                this.question.selectedObj = this.question.options.find((item) => item.key === object) || { key: "", value: "" };
            } else {
                this.question.selectedObj = object;
            }
        }
    }

    public registerOnChange(fn: any) {
        this.propagateChange = fn;
    }

    public propagateChange = (_: any) => { };

    public registerOnTouched() {
    }

    public openChange() {
        if (!this.question.disabled) {
            this.optionsOpen = !this.optionsOpen;
        }
    }

    private toggle(dd: any) {
        if (!this.question.disabled) {
            dd.toggle();
        }
    }
}

-----------------------------------------------------------------------

<div>
    <div (openChange)="openChange();" #dropDown="ngbDropdown" ngbDropdown class="wrapper" [ngClass]="{'disabled-item': question.disabled}">
        <input type="text" 
                [disabled]="question.disabled" 
                [name]="controlName" 
                class="select btn btn-outline-primary" 
                [ngModel]="question.selectedObj.value | translate"
                [title]="question.selectedObj.value"
                readonly ngbDropdownToggle #selectDiv/>
        <i (click)="toggle(dropDown);" [ngClass]="optionsOpen ? 'arrow-down' : 'arrow-up'" class="rchicons rch-003-button-icon-referenzen-pfeil-akkordon"></i>
        <div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="option-wrapper">
            <button *ngFor="let opt of question.options; trackBy: opt?.key" (click)="selectItem(opt.key, opt.value); dropDown.close();"
                class="dropdown-item option" [disabled]="question.disabled">{{opt.value | translate}}</button>
        </div>
    </div>
</div>

Karma config

var webpackConfig = require('./webpack/webpack.dev.js');

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine'],
    plugins: [
      require('karma-webpack'),
      require('karma-jasmine'),
      require('karma-phantomjs-launcher'),
      require('karma-sourcemap-loader'),
      require('karma-tfs-reporter'),
      require('karma-junit-reporter'),
    ],

    files: [
      './app/polyfills.ts',
      './tests-container/test-bundle.spec.ts',
    ],
    exclude: [],
    preprocessors: {
      './app/polyfills.ts': ['webpack', 'sourcemap'],
      './tests-container/test-bundle.spec.ts': ['webpack', 'sourcemap'],
      './app/**/!(*.spec.*).(ts|js)': ['sourcemap'],
    },
    webpack: {
      entry: './tests-container/test-bundle.spec.ts',
      devtool: 'inline-source-map',
      module: webpackConfig.module,
      resolve: webpackConfig.resolve
    },
    mime: {
      'text/x-typescript': ['ts', 'tsx']
    },

    reporters: ['progress', 'junit', 'tfs'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['PhantomJS'],
    singleRun: false,
    concurrency: Infinity
  })
}
Conference answered 7/12, 2017 at 9:28 Comment(6)
dont run fixture.detectChanges() inside beforeEachPortugal
I replaced the test in the question with another one. There I use fixture.detectChanges() only when I need to check/test the changed values, but the test take 15 seconds to run (on avarage 2 seconds for It section). Could it be the Karma setup/build the bottleneck?Conference
not exactly, it might be because your component takes that much time to initializePortugal
It could be, however also other components, even simpler than the one above in the question, take the same amount of time to run. Because of this I am thinking it has to do with the infrastructure of the tests, rather than the underlying components.Conference
Which browser do you use when running Angular tests?Almost
Headless ChromeConference
C
28

It turned out the problem is with Angular, as addressed on Github

Below a workaround from the Github discussion that dropped the time for running the tests from more than 40 seconds to just 1 second (!) in our project.

const oldResetTestingModule = TestBed.resetTestingModule;

beforeAll((done) => (async () => {
  TestBed.resetTestingModule();
  TestBed.configureTestingModule({
    // ...
  });

  function HttpLoaderFactory(http: Http) {
    return new TranslateHttpLoader(http, "/api/translations/", "");
  }

  await TestBed.compileComponents();

  // prevent Angular from resetting testing module
  TestBed.resetTestingModule = () => TestBed;
})()
  .then(done)
  .catch(done.fail));
Conference answered 7/12, 2017 at 12:36 Comment(2)
Sounds good to me. But how/when do you reset to the old resetTestingModule function? Does it work in all cases? - just found the solution regarding reset to old: https://mcmap.net/q/151794/-angular-4-unit-tests-testbed-extremely-slowLavoisier
@Lavoisier in all our cases this solution was working, however this can differ depending on the specific scenario. The approach proposed by Granfaloon below seems to be a good way to cover potential cases of tests conflict.Conference
C
9
describe('Test name', () => {
    configureTestSuite();

    beforeAll(done => (async () => {
       TestBed.configureTestingModule({
            imports: [HttpClientTestingModule, NgReduxTestingModule],
            providers: []
       });
       await TestBed.compileComponents();

    })().then(done).catch(done.fail));

    it(‘your test', (done: DoneFn) => {

    });
});

Create new file:

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

    export const configureTestSuite = () => {
       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;
       });

       afterAll(() => {
          TestBed.resetTestingModule = originReset;
          TestBed.resetTestingModule();
       });
    };
Conglobate answered 15/11, 2018 at 8:38 Comment(1)
I don't get the purpose of the code written in the afterEach block and in the after blockOveract
B
7

You may want to try out ng-bullet. It greatly increases execution speed of Angular unit tests. It's also suggested to be used in the official angular repo issue regarding Test Bed unit tests performance: https://github.com/angular/angular/issues/12409#issuecomment-425635583

The point is to replace the original beforeEach in the header of each test file

beforeEach(async(() => {
        // a really simplified example of TestBed configuration
        TestBed.configureTestingModule({
            declarations: [ /*list of components goes here*/ ],
            imports: [ /* list of providers goes here*/ ]
        })
        .compileComponents();
  }));

with configureTestSuite:

import { configureTestSuite } from 'ng-bullet';
...
configureTestSuite(() => {
    TestBed.configureTestingModule({
        declarations: [ /*list of components goes here*/ ],
        imports: [ /* list of providers goes here*/ ]
    })
});
Belletrist answered 1/2, 2019 at 10:7 Comment(1)
Thanks for sharing we copied the code and modified it to our project - awesome and simpleSilvan
S
6

Francesco's answer above is great, but it requires this code at the end. Otherwise other test suites will fail.

    afterAll(() => {
        TestBed.resetTestingModule = oldResetTestingModule;
        TestBed.resetTestingModule();
    });
Sambo answered 24/1, 2019 at 16:57 Comment(0)
P
1

October 2020 Update

Upgrading angular app to Angular 9 has a Massive test run time improvement,


and if you want to stay on the current version the below package helped me to improve test performance:

Ng-bullet link

Ng-Bullet is a library which enhances your unit testing experience with Angular TestBed, greatly increasing execution speed of your tests.

What it will do is it will not create the test suite all the time and will use the previously created suite and by using this I have seen the 300% improved test runs then before.

Ref

Passible answered 22/10, 2020 at 5:50 Comment(1)
Thanks to Ivy - Angular Unit Tests are much faster out of the box since the components are not re-compiled between tests, making it the test run much faster.Conference
E
1

If you are using Angular 12.1+ (if not then better to migrate to new version) then best way is just introduce teardown property which would surprisingly improve unittest execution speed because of below reasons:

  1. The host element is removed from the DOM
  2. Component styles are removed from the DOM
  3. Application-wide services are destroyed
  4. Feature-level services using the any provider scope are destroyed
  5. Angular modules are destroyed
  6. Components are destroyed
  7. Component-level services are destroyed
    All the above things will happen after each unittest execution.

Just open you test-main.ts file and put below code:

getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting(),
  { teardown: { destroyAfterEach: true } }, 
);
Emileemilee answered 7/1, 2022 at 10:27 Comment(2)
Thanks for your comment. However the questions was made 4 years ago...that project is long closed now :) We could however already find a good solution back in the time (see my answer below)Conference
this is just for your info! As everyone is keen to improve the process now. Its great that you already reached up-to that level.Emileemilee
N
0

I made a little function you can use to speed things up. Its effect is similar to ng-bullet mentioned in other answers, but still cleans up services between tests so that they cannot leak state. The function is precompileForTests, available in n-ng-dev-utils.

Use it like this (from its docs):

// let's assume `AppModule` declares or imports a `HelloWorldComponent`
precompileForTests([AppModule]);

// Everything below here is the same as normal. Just add the line above.

describe("AppComponent", () => {
  it("says hello", async () => {
    TestBed.configureTestingModule({ declarations: [HelloWorldComponent] });
    await TestBed.compileComponents(); // <- this line is faster
    const fixture = TestBed.createComponent(HelloWorldComponent);
    expect(fixture.nativeElement.textContent).toContain("Hello, world!");
  });
});
Noranorah answered 28/6, 2019 at 23:17 Comment(4)
I don't understand this first line precompileForTests... Why does it have an AppModule?Meatiness
You should specify your main module there, that has everything in your app. Then it will precompile all of it for all your tests up front, instead of recompiling before each test.Noranorah
I really don't understand this, because your tests should be isolated from any real life implementation, right?Meatiness
That's a matter of taste, but regardless this does not add/remove any isolation from the tests themselves. It will behave just as without the precomileForTests() call, except that when angular would normally compile HelloWorldComponent it won't have to, because it was already precompiled.Noranorah
D
0

Yoav Schniederman answer was helpful for me. To add on we need to clean <style> in our <head> tag as they are also responsible for memory leak.Cleaning all styles in afterAll() also improved performance to a good extend.

Please read original post for reference

Divisibility answered 17/9, 2020 at 12:4 Comment(1)
Please add code and data as text (using code formatting), not images. Images: A) don't allow us to copy-&-paste the code/errors/data for testing; B) don't permit searching based on the code/error/data contents; and many more reasons. Images should only be used, in addition to text in code format, if having the image adds something significant that is not conveyed by just the text code/error/data.Fibriform
B
0

In my specific case it was delaying because we were importing our styles.scss (which also was importing other huge styles) in our components styles 'component.component.scss', this generates recursive styles for every component template.

To avoid this, only import scss variables, mixins and similar stuff in your components.

Blackbeard answered 10/3, 2021 at 13:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.