首页 > 解决方案 > 为什么我需要调用detectChanges / whenStable 两次?

问题描述

第一个例子

我有以下测试:

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

import { Component } from '@angular/core';

@Component({
    template: '<ul><li *ngFor="let state of values | async">{{state}}</li></ul>'
})
export class TestComponent {
    values: Promise<string[]>;
}

describe('TestComponent', () => {
    let component: TestComponent;
    let fixture: ComponentFixture<TestComponent>;
    let element: HTMLElement;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            declarations: [TestComponent]
        })
            .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
        element = (<HTMLElement>fixture.nativeElement);
    });

    it('this test fails', async() => {
        // execution
        component.values = Promise.resolve(['A', 'B']);
        fixture.detectChanges();
        await fixture.whenStable();

        // evaluation
        expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
    });

    it('this test works', async() => {
        // execution
        component.values = Promise.resolve(['A', 'B']);
        fixture.detectChanges();
        await fixture.whenStable();
        fixture.detectChanges();
        await fixture.whenStable();

        // evaluation
        expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
    });
});

如您所见,有一个超级简单的组件,它仅显示由Promise. 有两种测试,一种失败,一种通过。这些测试之间的唯一区别是通过fixture.detectChanges(); await fixture.whenStable();两次调用的测试。

更新:第二个例子(2019/03/21 再次更新)

这个例子试图调查与 ngZone 的可能关系:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { Component, NgZone } from '@angular/core';

@Component({
    template: '{{value}}'
})
export class TestComponent {
    valuePromise: Promise<ReadonlyArray<string>>;
    value: string = '-';

    set valueIndex(id: number) {
        this.valuePromise.then(x => x).then(x => x).then(states => {
            this.value = states[id];
            console.log(`value set ${this.value}. In angular zone? ${NgZone.isInAngularZone()}`);
        });
    }
}

describe('TestComponent', () => {
    let component: TestComponent;
    let fixture: ComponentFixture<TestComponent>;

    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [FormsModule],
            declarations: [TestComponent],
            providers: [
            ]
        })
            .compileComponents();
    }));

    beforeEach(() => {
        fixture = TestBed.createComponent(TestComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

    function diagnoseState(msg) {
        console.log(`Content: ${(fixture.nativeElement as HTMLElement).textContent}, value: ${component.value}, isStable: ${fixture.isStable()} # ${msg}`);
    }

    it('using ngZone', async() => {
        // setup
        diagnoseState('Before test');
        fixture.ngZone.run(() => {
            component.valuePromise = Promise.resolve(['a', 'b']);

            // execution
            component.valueIndex = 1;
        });
        diagnoseState('After ngZone.run()');
        await fixture.whenStable();
        diagnoseState('After first whenStable()');
        fixture.detectChanges();
        diagnoseState('After first detectChanges()');
    });

    it('not using ngZone', async(async() => {
        // setup
        diagnoseState('Before setup');
        component.valuePromise = Promise.resolve(['a', 'b']);

        // execution
        component.valueIndex = 1;

        await fixture.whenStable();
        diagnoseState('After first whenStable()');
        fixture.detectChanges();
        diagnoseState('After first detectChanges()');

        await fixture.whenStable();
        diagnoseState('After second whenStable()');
        fixture.detectChanges();
        diagnoseState('After second detectChanges()');

        await fixture.whenStable();
        diagnoseState('After third whenStable()');
        fixture.detectChanges();
        diagnoseState('After third detectChanges()');
    }));
});

这些测试中的第一个(明确使用 ngZone)导致:

Content: -, value: -, isStable: true # Before test
Content: -, value: -, isStable: false # After ngZone.run()
value set b. In angular zone? true
Content: -, value: b, isStable: true # After first whenStable()
Content: b, value: b, isStable: true # After first detectChanges()

第二个测试日志:

Content: -, value: -, isStable: true # Before setup
Content: -, value: -, isStable: true # After first whenStable()
Content: -, value: -, isStable: true # After first detectChanges()
Content: -, value: -, isStable: true # After second whenStable()
Content: -, value: -, isStable: true # After second detectChanges()
value set b. In angular zone? false
Content: -, value: b, isStable: true # After third whenStable()
Content: b, value: b, isStable: true # After third detectChanges()

我有点期望测试在角度区域中运行,但事实并非如此。问题似乎来自以下事实

为了避免意外,传递给 then() 的函数永远不会被同步调用,即使是已经解决的 promise。(来源

在第二个示例中,我通过.then(x => x)多次调用来引发问题,这只不过是将进度再次放入浏览器的事件循环中,从而延迟结果。到目前为止,据我所知,调用await fixture.whenStable()基本上应该说“等到该队列为空”。正如我们所看到的,如果我明确地在 ngZone 中执行代码,这实际上是有效的。然而,这不是默认设置,我在手册中找不到我打算以这种方式编写测试的任何地方,所以这感觉很尴尬。

await fixture.whenStable()在第二次测试中实际上做了什么?源代码显示,在这种情况下将fixture.whenStable()只是return Promise.resolve(false);. 所以我实际上尝试替换await fixture.whenStable()await Promise.resolve(),实际上它具有相同的效果:这确实具有暂停测试并从事件队列开始的效果,因此如果我经常调用任何承诺,传递给的回调valuePromise.then(...)实际上会被执行await足够的。

为什么我需要await fixture.whenStable();多次调用?我用错了吗?这是预期的行为吗?是否有任何关于它打算如何工作/如何处理这个问题的“官方”文档?

标签: angularangular-testtestbed

解决方案


相信你正在经历Delayed change detection

延迟变更检测是有意的和有用的。它让测试人员有机会在 Angular 启动数据绑定和调用生命周期钩子之前检查和更改组件的状态。

检测变化()


实施允许您在两个测试Automatic Change Detection中只调用一次。fixture.detectChanges()

 beforeEach(async(() => {
            TestBed.configureTestingModule({
                declarations: [TestComponent],
                providers:[{ provide: ComponentFixtureAutoDetect, useValue: true }] //<= SET AUTO HERE
            })
                .compileComponents();
        }));

堆栈闪电战

https://stackblitz.com/edit/directive-testing-fnjjqj?embed=1&file=app/app.component.spec.ts

示例中的此注释Automatic Change Detection很重要,以及为什么您的测试仍需要调用fixture.detectChanges(),即使使用AutoDetect.

第二个和第三个测试揭示了一个重要的限制。Angular 测试环境不知道测试改变了组件的标题。ComponentFixtureAutoDetect 服务响应异步活动,例如承诺解析、计时器和 DOM 事件。但是组件属性的直接、同步更新是不可见的。测试必须手动调用 fixture.detectChanges() 来触发另一个变化检测周期。

由于您在设置 Promise 时解决它的方式,我怀疑它被视为同步更新并且Auto Detection Service不会响应它。

component.values = Promise.resolve(['A', 'B']);

自动变化检测


检查给出的各种示例提供了一个线索,说明为什么需要在fixture.detectChanges()没有AutoDetect. 第一次ngOnInitDelayed change detection模型中触发......第二次调用它会更新视图。

fixture.detectChanges()您可以根据下面代码示例中右侧的注释看到这一点

it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {
  fixture.detectChanges(); // ngOnInit()
  expect(quoteEl.textContent).toBe('...', 'should show placeholder');

  tick(); // flush the observable to get the quote
  fixture.detectChanges(); // update view

  expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
  expect(errorMessage()).toBeNull('should not show error');
}));

更多异步测试示例


总结: 当不利用Automatic change detection时,调用fixture.detectChanges()将“逐步”通过Delayed Change Detection模型......让您有机会在 Angular 启动数据绑定和调用生命周期钩子之前检查和更改组件的状态。

另请注意所提供链接中的以下评论:

本指南中的示例总是显式调用detectChanges(),而不是想知道测试夹具何时会或不会执行更改检测。比严格必要的更频繁地调用 detectChanges() 并没有什么坏处。


第二个例子 Stackblitz

第二个示例 stackblitz 显示注释掉第 53 行detectChanges()会产生相同的console.log输出。之前调用detectChanges()两次whenStable()是没有必要的。你打detectChanges()了三次,但之前的第二次电话whenStable()没有任何影响。detectChanges()在您的新示例中,您只是真正从其中的两个中获得任何东西。

比严格必要的更频繁地调用 detectChanges() 并没有什么坏处。

https://stackblitz.com/edit/directive-testing-cwyzrq?embed=1&file=app/app.component.spec.ts


更新:第二个例子(2019/03/21 再次更新)

提供 stackblitz 以演示以下变体的不同输出以供您查看。

  • 等待fixture.whenStable();
  • fixture.whenStable().then(()=>{})
  • 等待fixture.whenStable().then(()=>{})

堆栈闪电战

https://stackblitz.com/edit/directive-testing-b3p5kg?embed=1&file=app/app.component.spec.ts


推荐阅读