为什么我需要两次调用detectChanges / whenStable?

时间:2019-03-16 10:28:00

标签: angular angular-test testbed

第一个示例

我已经进行了以下测试:

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。 (Source

在第二个示例中,我多次调用.then(x => x)引发了问题,这只不过是将进度再次放入浏览器的事件循环中而导致结果延迟。根据我的理解,到目前为止,对await fixture.whenStable()的调用应基本上说“等到该队列为空”。如我们所见,如果我在ngZone中显式执行代码,则此方法确实有效。但是,这不是默认值,因此我无法在手册中的任何地方找到以这种方式编写测试的意图,因此感觉很尴尬。

await fixture.whenStable()在第二次测试中实际上做什么? source code显示,在这种情况下,fixture.whenStable()将只是return Promise.resolve(false);。因此,我实际上尝试用await fixture.whenStable()替换await Promise.resolve()并确实具有相同的效果:这确实具有挂起测试并从事件队列开始的作用,因此回调传递给了{{1} }实际上已经执行过,只要我经常按任何承诺调用valuePromise.then(...)即可。

为什么我需要多次致电await?我使用错了吗?这是预期的行为吗?是否有任何有关它打算如何工作/如何处理的“官方”文档?

2 个答案:

答案 0 :(得分:10)

我相信您遇到了Delayed change detection

  

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

detectChanges()


实施Automatic Change Detection可使您在两个测试中仅调用一次fixture.detectChanges()

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

Stackblitz

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

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

  

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

由于设置时解决Promise的方式,我怀疑它被视为同步更新,而Auto Detection Service不会对此做出响应。

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

Automatic Change Detection


检查给出的各种示例可以提供一个线索,说明为什么需要不使用fixture.detectChanges()两次调用AutoDetect。第一次触发ngOnInit模型中的Delayed 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');
}));

More async tests Example


摘要: 当不使用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(()=> {})

Stackblitz

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

答案 1 :(得分:0)

我认为第二项测试似乎是错误的,应按照以下模式编写:

component.values = Promise.resolve(['A', 'B']);
fixture.whenStable().then(() => {
  fixture.detectChanges();       
  expect(Array.from(element.querySelectorAll('li')).map(elem => elem.textContent)).toEqual(['A', 'B']);
});

请参阅:When Stable Usage

您应在detectChanges之内致电whenStable()

  

fixture.whenStable()返回一个承诺,该承诺在JavaScript引擎的任务队列为空时解析。