Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 4 Unit Tests (TestBed) extremely slow

Tags:

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   }) } 
like image 562
Francesco Avatar asked Dec 07 '17 09:12

Francesco


People also ask

What is TestBed in unit testing?

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.

What is resetTestingModule?

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.

How long should unit test suite take to run?

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.


1 Answers

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)); 
like image 170
Francesco Avatar answered Sep 25 '22 22:09

Francesco