Componentのテストで頻出するパターンをまとめると、次のようになります。
- MicroTask(Promise.then)を実行させたい: fixture.whenStable, fakeAsync + flushMicrotasks, fakeAsync + tick
- MacroTaskの完了を待ちたい: fixture.whenStable
- MacroTask(但しタイマー系のみ)を実行したい: fakeAsync + tick
- EventTask(但しDOMのみ)を強制的に実行したい: fixture.query(...).triggerEventHandler
各taskの終了を待った後、fixture.detectChanges を実行すれば、テスト対象のComponentとそのViewを狙った状態にすることが出来ると思います。 AppricationRefの挙動である各taskの実行後で、且つMicroTaskが空の場合に変更検知を行うを押えておけばどうということはありません6。
当然と言えば当然なのですが、XHRのようにブラウザの外部と通信するようなケースはどうしようもないので、serviceをmockingするなり、HttpBackendを使うなり、JasmineのspyOnを使うなり工夫しましょう。
まとめ
- Zone.jsは非同期処理をinterceptする機能を持ったユーティリティ
- MicroTask(Promise), MacroTask(timer系), EventTask(DOM等)の3種類
- AngularはZoneのtask状態を監視して変更検知を実行している
- テストコードではfixture.whenStable や tick, flushMicrotasks, triggerEventHandlerでtask実行を制御し、その後 fixture.detectChangesを実行するようにする
Jasmine Observable Test
it('should be done after subscribe', (done: DoneFn) => {
service.getSomeObservable().subscribe(x => {
expect(x).toBe('expected');
done();
});
});
Jasmine Mocking Pattern
class Service {
constructor(private valueService) { }
getValue() { return this.valueService.getValue(); }
}
it('should return mocked value.', () => {
service = new Service(new ValueService());
expect(service.getValue()).toBe('real value');
service = new Service(new FakeValueService());
expect(service.getValue()).toBe('fake value');
service = new Service({ getValue: () => 'fake obj value' });
expect(service.getValue()).toBe('fake obj value');
const spy = jasmine.createSpyObj('ValueService', ['getValue']);
spy.getValue.and.returnValue('stub value');
service = new Service(spy);
expect(service.getValue()).toBe('stub value');
expect(spy.getValue.calls.count).toBe(1);
expect(spy.getValue.calls.mostRecent().returnValue).toBe('stub value');
});
TestBed tips
providers: [
Service,
{ provide: ValueService, useValue: spy }
]
service = TestBed.get(Service); // injected spy object
Helper
asyncData(value); // return Promised Value (defer(() => Promise.resolve(value)))
asyncError(value); // return Promised Error ....reject(value)
Http
// if you use HttpClientTestingModule, you don't do below.
httpClientSpy.get.and.returnValue(asyncError(errorResponse));
service.request().subscribe(_ => fail('unreachable'), err => {
expect(err).toBe(errorResponse);
})
dispatchEvent(new Event()) in IE11
if(typeof(Event) === 'function') {
var event = new Event('submit');
}else{
var event = document.createEvent('Event');
event.initEvent('submit', true, true);
}
$el.dispatchEvent(event);
// other method
heroDe.triggerEventHandler('click', null);
/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */
export const ButtonClickEvents = {
left: { button: 0 },
right: { button: 2 }
};
/** Simulate element click. Defaults to mouse left-button click event. */
export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
if (el instanceof HTMLElement) {
el.click();
} else {
el.triggerEventHandler('click', eventObj);
}
}
click(heroDe)
ComponentTest
// You MUST call this if your copmonent has templateUrl, stylesUrl.
TestBed.compileComponents();
// get Text
fiture.nativeElement.querySelector('.hoge').textContent
// Async test **except** for XHR
it('hoge', fakeAsync(() => {
fixture.detectChanges(); // until ngOnInit()
tick(); // wait for setTimeout or next observable
fixture.detectChanges(); // after setTimeout
}));
//
it('foo', async(() => {
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
});
}));
Advanced Typescript tips
// Partial
interface User { a:any, b:any, c:any }
let user: Partial<User> = { c:'hoge' };
// Partial<User> -> User { a?:any, b?:any, c?:any }
Matcher
toMatch(/regex/, 'target');