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 }) }
The TestBed is the first and largest of the Angular testing utilities. It creates an Angular testing module — a @NgModule class — that you configure with the configureTestingModule method to produce the module environment for the class you want to test.
But what does the TestBed. resetTestingModule function actually do? It cleans up all your overrides, modules, module factories and disposes all active fixtures as well. If only we could keep the compiled factories, and just re-create components and services without re-compilation.
Still, it seems as though a 10 second short-term attention span is more or less hard-wired into the human brain. Thus, a unit test suite used for TDD should run in less than 10 seconds. If it's slower, you'll be less productive because you'll constantly lose focus.
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));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With