【问题标题】:Why do I need to call detectChanges / whenStable twice?为什么我需要调用detectChanges / whenStable 两次?
【发布时间】:2019-08-07 07:24:37
【问题描述】:

第一个例子

我有以下测试:

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

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

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

【问题讨论】:

  • 在我的应用程序中很多情况下我都有同样的问题并放弃了,只是添加了两次 :) 看看是否有人在这里解决它会很有趣!
  • 这似乎与 promise 和 resolve 的工作方式有关。有趣的是,使用 observable 而不是你不需要触发 detectChanges 两次的 promise。知道为什么会很有趣。 stackblitz.com/edit/directive-testing-1bdxlz

标签: angular angular-test testbed


【解决方案1】:

相信您正在体验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();
        }));

堆栈闪电战

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

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

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

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

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

Automatic Change Detection


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

您可以根据右侧的 cmets 看到这一点 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 输出。在whenStable() 之前调用detectChanges() 两次是不必要的。您拨打了三次detectChanges(),但whenStable() 之前的第二次呼叫没有任何影响。在您的新示例中,您只能从两个 detectChanges() 中真正获得任何东西。

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

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


更新:第二个示例(2019 年 3 月 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

【讨论】:

  • 虽然这些信息非常好,但它们并没有真正回答我为什么需要两次调用这些函数的问题。当然自动变化检测无法检测到属性变化之类的东西,所以我需要调用detectChanges(),但是为什么要调用两次呢?与此同时,我做了更多研究,并在我的问题中添加了第二个示例。
  • 您第一次调用detectChanges()NgOninit,您第二次调用detectChanges() 会更新视图并启动data binding。您的附加示例说明了这一事实。
  • detectChanges() 的第一次调用是在beforeEach 块的第27 行(第一个示例)和第37 行(第二个示例)中调用NgOnInit。如果你把那个算进去,那么问题是为什么我需要调用那个函数三次。
  • 在“设置索引后”行之后的第 53 行中注释掉 detectChanges(),您会注意到无论有没有此 detectChanges() 行,您的输出都是相同的。在whenStable() 之前调用detectChanges() 两次不会做任何额外的事情,而且是不必要的。在您的示例中,您实际上只使用了两次detectChanges(),即使您调用了它三次。
  • 提供了第二个 stackblitz 示例来说明注释掉第 53 行 detectChanges() 会产生相同的 console.log 输出。
【解决方案2】:

在我看来,第二个测试似乎是错误的,应该按照这种模式编写:

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

您应该在whenStable() 中调用detectChanges

fixture.whenStable() 返回一个 Promise,当 JavaScript 引擎的任务队列为空时会解析。

【讨论】:

  • 这与写await fixture.whenStable(); fixture.detectChanges();而不是fixture.detectChanges(); await fixture.whenStable();基本相同,这不会改变测试结果。
  • 那么上面的例子还是失败了?
  • 是的,它仍然失败
  • 我认为您的“稳定使用时”链接可能已被移动或更改。我在那个页面上再也找不到关于 whenStable() 的任何信息了。
【解决方案3】:

我发现这个问题是因为我花了几个小时调试为什么我需要在我的测试用例中多次编写 detectChanges / whenStable

我从@Marshal 的回答中了解了“自动更改检测”(使用ComponentFixtureAutoDetect )并进行了尝试。但仍然无法使用它,因为我的组件有一些@Input 需要并在ngOnInit 中使用。启用ComponentFixtureAutoDetect 会导致那里出现错误,因为一旦我这样做TestBed.createComponent() - 它运行ngOnInit() 会导致错误。

我最终制作了一个辅助函数,使用.autoDetectChanges (reference) 为单个调用临时启用此功能:

async autoWhenStable<C>(fixture: ComponentFixture<C>) {
    fixture.autoDetectChanges(true);
    await fixture.whenStable();
    fixture.autoDetectChanges(false);
}

这似乎有助于我编写测试用例,因为我不必担心必须调用 whenStable 多少次。
并且感觉比大多数地方建议的黑客要好得多,只是说做很多次(因为多调用几次也没有什么坏处):

async robustWhenStable<C>(fixture: ComponentFixture<C>) {
    for (let i = 0; i < 10; i++) {
        fixture.detectChanges();
        await fixture.whenStable();
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-04-03
    • 2017-12-27
    • 1970-01-01
    • 2022-07-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多