简体   繁体   English

Angular 4 单元测试 (TestBed) 非常慢

[英]Angular 4 Unit Tests (TestBed) extremely slow

I have some unit tests using Angular TestBed.我有一些使用 Angular TestBed 的单元测试。 Even if the tests are very simple, they run extremely slow (on avarage 1 test assetion per second).即使测试非常简单,它们的运行速度也非常慢(平均每秒 1 次测试)。
Even after re-reading Angular documentation, I could not find the reason of such a bad perfomance.即使在重新阅读 Angular 文档后,我也找不到性能如此糟糕的原因。

Isolated tests, not using TestBed, run in a fraction of second.不使用 TestBed 的隔离测试可在几分之一秒内运行。

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
  })
}

It turned out the problem is with Angular, as addressed on Github事实证明问题出在 Angular 上,如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.下面是来自 Github 讨论的解决方法,该解决方法将我们项目中运行测试的时间从 40 多秒减少到仅 1 秒(!)

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));
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();
       });
    };

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();
    });

You may want to try out ng-bullet .你可能想试试ng-bullet It greatly increases execution speed of Angular unit tests.它大大提高了 Angular 单元测试的执行速度。 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还建议在有关测试床单元测试性能的官方角度回购问题中使用它: 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

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 :使用configureTestSuite

import { configureTestSuite } from 'ng-bullet';
...
configureTestSuite(() => {
    TestBed.configureTestingModule({
        declarations: [ /*list of components goes here*/ ],
        imports: [ /* list of providers goes here*/ ]
    })
});

October 2020 Update 2020 年 10 月更新

Upgrading angular app to Angular 9 has a Massive test run time improvement ,将 angular 应用程序升级到 Angular 9大量的测试运行时间改进


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. Ng-Bullet 是一个使用 Angular TestBed 增强单元测试体验的库,大大提高了测试的执行速度。

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.它会做的是它不会一直创建测试套件,而是使用之前创建的套件,通过使用它,我已经看到了 300% 改进的测试运行。

Ref 参考

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.它的效果类似于其他答案中提到的ng-bullet ,但仍会在测试之间清理服务,以便它们不会泄漏状态。 The function is precompileForTests , available in n-ng-dev-utils .该函数是precompileForTests ,在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!");
  });
});

Yoav Schniederman answer was helpful for me. Yoav Schniederman 的回答对我很有帮助。 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.另外,我们需要清理<style> <head>标签中的<style> ,因为它们也是内存泄漏的原因。 afterAll()所有样式也可以很好地提高性能。

Please read original post for reference请阅读原帖以供参考

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.在我的特定情况下,它会延迟,因为我们在组件样式“component.component.scss”中导入了我们的 style.scss(也导入了其他巨大的样式),这会为每个组件模板生成递归样式。

To avoid this, only import scss variables, mixins and similar stuff in your components.为了避免这种情况,只在你的组件中导入 scss 变量、mixin 和类似的东西。

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:如果您使用的是Angular 12.1+ (如果不是,那么最好迁移到新版本)那么最好的方法就是引入teardown属性,由于以下原因,这会令人惊讶地提高单元测试的执行速度:

  1. The host element is removed from the DOM宿主元素从 DOM 中移除
  2. Component styles are removed from the DOM组件样式从 DOM 中移除
  3. Application-wide services are destroyed应用程序范围的服务被销毁
  4. Feature-level services using the any provider scope are destroyed使用 any provider 范围的功能级服务被销毁
  5. Angular modules are destroyed Angular 模块被销毁
  6. Components are destroyed组件被破坏
  7. Component-level services are destroyed组件级服务被销毁
    All the above things will happen after each unittest execution.上述所有事情都会在每次 unittest 执行后发生。

Just open you test-main.ts file and put below code:只需打开您的test-main.ts文件并输入以下代码:

getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting(),
  { teardown: { destroyAfterEach: true } }, 
);

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 Angular 2 单元测试:如何覆盖 TestBed 中的单个提供程序(用于服务的单元测试,而不是具有服务依赖项的组件)? - Angular 2 unit tests: How to override a single provider in TestBed (for unit tests for a service, not a component, with service dependencies)? Angular 单元测试使用 TestBed.inject 根据测试执行顺序成功/失败 - Angular Unit Tests using TestBed.inject succeed/fail according to tests execution order Angular 中没有 TestBed 和组件的单元测试指令 - Unit Tesing directives in Angular without TestBed & Component 带有.a​​sObservable()的Angular 2单元测试(TestBed)服务 - Angular 2 Unit Test (TestBed) service with .asObservable() 如何使用 TestBed 和 Jasmine 在 NativeScript 中实现单元测试? - How to implement unit tests in NativeScript using TestBed and Jasmine? Angular 8 - 嵌套的 ngFor 非常慢 - Angular 8 - nested ngFor is extremely slow Angular 单元测试 - 如何通过在 TestBed 提供程序中使用 useValue 将依赖项注入到 TestBed - Angular Unit Test - How to inject dependencies to TestBed by using useValue in the TestBed provider Angular 6 和 Jest 的测试出错 - TypeError: testing.TestBed.inject is not a function - Error in tests with Angular 6 and Jest - TypeError: testing.TestBed.inject is not a function 如何在没有 TestBed 的情况下为 Angular 组件编写单元笑话测试? - How to write unit jest test for Angular Component without TestBed? Angular/NgRx 单元测试无法创建 TestBed - Angular/NgRx Unit testing can't create TestBed
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM