Reputation: 10830
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
})
}
Upvotes: 28
Views: 18478
Reputation: 2702
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:
Just open you test-main.ts
file and put below code:
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
{ teardown: { destroyAfterEach: true } },
);
Upvotes: 1
Reputation: 1
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.
Upvotes: 0
Reputation: 1208
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 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.
Upvotes: 1
Reputation: 1354
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
Upvotes: 0
Reputation: 10830
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));
Upvotes: 28
Reputation: 6029
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!");
});
});
Upvotes: 0
Reputation: 375
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*/ ]
})
});
Upvotes: 7
Reputation: 61
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();
});
Upvotes: 6
Reputation: 5391
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();
});
};
Upvotes: 9