私は次のテストを受けました:
_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
によって提供されるアイテムのリストを表示するだけの非常にシンプルなコンポーネントがあります。 2つのテストがあり、1つは失敗し、もう1つは合格です。これらのテストの唯一の違いは、合格したテストがfixture.detectChanges(); await fixture.whenStable();
を2回呼び出すことです。
この例では、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()
_
2番目のテストログ:
_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()
_
テストがangular=ゾーンで実行されることを少し期待していましたが、そうではありません。問題は、
驚きを避けるために、then()に渡される関数は、すでに解決済みのプロミスがある場合でも、同期的に呼び出されることはありません。 ( ソース )
この2番目の例では、.then(x => x)
を複数回呼び出すことで問題を引き起こしました。これは、進行状況をブラウザーのイベントループに再度入れ、結果を遅らせるだけです。これまでの私の理解では、await fixture.whenStable()
の呼び出しは、基本的に「そのキューが空になるまで待機する」と言う必要があります。 ngZoneでコードを明示的に実行すると、これは実際に機能します。ただし、これはデフォルトではなく、テストをそのように記述することを意図したマニュアルのどこにも見つけることができないため、これは扱いにくいと感じます。
2番目のテストで実際にawait fixture.whenStable()
は何をしますか? ソースコード は、この場合fixture.whenStable()
がreturn Promise.resolve(false);
になることを示しています。したがって、私は実際にawait fixture.whenStable()
をawait Promise.resolve()
で置き換えようとしましたが、実際には同じ効果があります。これは、テストを一時停止し、イベントキューで開始する効果があるため、コールバックに渡されますvaluePromise.then(...)
が実際に実行されるのは、約束どおりにawait
を呼び出すだけで十分な場合です。
await fixture.whenStable();
を複数回呼び出す必要があるのはなぜですか?私はそれを間違って使用していますか?これは意図された動作ですか?これがどのように機能するか/どのように対処するかについての「公式」文書はありますか?
_Delayed change detection
_が発生していると思います。
遅延変更の検出は意図的で便利です。 Angularがデータバインディングを開始し、ライフサイクルフックを呼び出す前に、コンポーネントの状態を検査および変更する機会をテスターに与えます。
_Automatic Change Detection
_を実装すると、両方のテストでfixture.detectChanges()
を1回だけ呼び出すことができます。
_ 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
_の例のこのコメントは重要であり、AutoDetect
を使用しても、テストでfixture.detectChanges()
を呼び出す必要がある理由は重要です。
2番目と3番目のテストは、重要な制限を明らかにします。 Angularテスト環境は、テストがコンポーネントのタイトルを変更したことを認識していません。ComponentFixtureAutoDetectサービスは、promise解決、タイマー、DOMイベントなどの非同期アクティビティに応答します。ただし、直接の同期更新コンポーネントプロパティは非表示です。テストでは、変更検出の別のサイクルをトリガーするために手動でfixture.detectChanges()を呼び出す必要があります。
Promiseを設定する際にPromiseを解決する方法が原因で、それが同期更新として処理されており、_Auto Detection Service
_がそれに応答しないと思います。
_component.values = Promise.resolve(['A', 'B']);
_
与えられたさまざまな例を調べると、AutoDetect
なしでfixture.detectChanges()
を2回呼び出す必要がある理由がわかります。最初に_Delayed change detection
_モデルでngOnInit
をトリガーします... 2回目に呼び出すと、ビューが更新されます。
これは、以下のコード例の
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()を呼び出しても害はありません。
2番目の例Stackblitz
53行目のdetectChanges()
をコメントアウトすると同じ_console.log
_出力が生成されることを示す2番目の例のstackblitz。 detectChanges()
の前にwhenStable()
を2回呼び出す必要はありません。 detectChanges()
を3回呼び出していますが、whenStable()
の前の2番目の呼び出しには影響がありません。新しい例では、本当にdetectChanges()
の2つから何かを得ているだけです。
厳密に必要な場合よりも頻繁にdetectChanges()を呼び出しても害はありません。
https://stackblitz.com/edit/directive-testing-cwyzrq?embed=1&file=app/app.component.spec.ts
UPDATE:2番目の例(2019/03/21に再度更新)
以下のバリアントからのさまざまな出力を確認できるように、stackblitzを提供します。
Stackblitz
https://stackblitz.com/edit/directive-testing-b3p5kg?embed=1&file=app/app.component.spec.ts
私の意見では、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']);
});
_
ご覧ください: 安定使用時
whenStable()
内でdetectChanges
を次のように呼び出す必要があります
Fixture.whenStable()は、JavaScriptエンジンのタスクキューが空になると解決するpromiseを返します。