Karma-JasmineでAngular 2 テスト:非同期サービス呼び出しをテストする方法
Angular 2 テストにおける非同期関数呼び出し:いつ使うべきか?
Karma-Jasmine と async テストを使用する一般的なシナリオは以下の通りです。
非同期サービス呼び出しのテスト
コンポーネントが非同期サービスに依存している場合、サービスの応答をシミュレートし、コンポーネントが期待通りに動作することを確認する必要があります。
// karma-jasmine.conf.js
...
singleRun: false,
autoWatch: true,
browsers: ['Chrome'],
...
// component.spec.ts
import { Component, OnInit } from '@angular/core';
import { MyService } from '../my-service';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent implements OnInit {
data: any;
constructor(private myService: MyService) { }
ngOnInit() {
this.myService.getData().then(data => this.data = data);
}
}
// component.spec.ts
describe('MyComponent', () => {
let component: MyComponent;
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [
{ provide: MyService, useValue: jasmine.createSpyObj('MyService', ['getData'])}
]
});
component = TestBed.createComponent(MyComponent);
service = TestBed.inject(MyService);
});
it('should load data from service', async () => {
const mockData = { value: 1 };
service.getData.and.returnValue(Promise.resolve(mockData));
await component.fixture.detectChanges(); // コンポーネントの変更を検出
expect(component.data).toEqual(mockData);
});
});
タイマーを使用した処理のテスト
コンポーネントが setTimeout
や setInterval
などのタイマー関数を使用して非同期処理を実行する場合、これらの関数の動作をシミュレートし、コンポーネントが期待通りに動作することを確認する必要があります。
// component.spec.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent implements OnInit {
count: number = 0;
ngOnInit() {
setInterval(() => this.count++, 1000);
}
}
// component.spec.ts
describe('MyComponent', () => {
let component: MyComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent]
});
component = TestBed.createComponent(MyComponent);
});
it('should increment count every second', fakeAsync(() => {
jasmine.clock().tick(1000); // 1秒経過
component.fixture.detectChanges();
expect(component.count).toBe(1);
jasmine.clock().tick(1000); // もう1秒経過
component.fixture.detectChanges();
expect(component.count).toBe(2);
}));
});
DOM イベントのテスト
コンポーネントが DOM イベントに依存している場合、イベントをトリガーし、コンポーネントが期待通りに反応することを確認する必要があります。
// component.spec.ts
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent {
@Output() clickEvent = new EventEmitter<void>();
onClick() {
this.clickEvent.emit();
}
}
// component.spec.ts
describe('MyComponent', () => {
let component: MyComponent;
let clickSpy: jasmine.Spy;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent]
});
component
// component.ts
import { Component, OnInit } from '@angular/core';
import { MyService } from '../my-service';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent implements OnInit {
data: any;
constructor(private myService: MyService) { }
ngOnInit() {
this.myService.getData().then(data => this.data = data);
}
}
// component.spec.ts
import { Component, OnInit } from '@angular/core';
import { MyService } from '../my-service';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent implements OnInit {
data: any;
constructor(private myService: MyService) { }
ngOnInit() {
this.myService.getData().then(data => this.data = data);
}
}
// component.spec.ts
describe('MyComponent', () => {
let component: MyComponent;
let service: MyService;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [
{ provide: MyService, useValue: jasmine.createSpyObj('MyService', ['getData'])}
]
});
component = TestBed.createComponent(MyComponent);
service = TestBed.inject(MyService);
});
it('should load data from service', async () => {
const mockData = { value: 1 };
service.getData.and.returnValue(Promise.resolve(mockData));
await component.fixture.detectChanges(); // コンポーネントの変更を検出
expect(component.data).toEqual(mockData);
});
});
この例では、MyComponent
コンポーネントが setInterval
を使用して1秒ごとにカウンターをインクリメントする方法をテストします。
// component.ts
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent implements OnInit {
count: number = 0;
ngOnInit() {
setInterval(() => this.count++, 1000);
}
}
// component.spec.ts
describe('MyComponent', () => {
let component: MyComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent]
});
component = TestBed.createComponent(MyComponent);
});
it('should increment count every second', fakeAsync(() => {
jasmine.clock().tick(1000); // 1秒経過
component.fixture.detectChanges();
expect(component.count).toBe(1);
jasmine.clock().tick(1000); // もう1秒経過
component.fixture.detectChanges();
expect(component.count).toBe(2);
}));
});
DOM イベント
この例では、MyComponent
コンポーネントがボタンクリックイベントをどのように処理するかをテストします。
// component.ts
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent {
@Output() clickEvent = new EventEmitter<void>();
onClick() {
this.clickEvent.emit();
}
}
// component.spec.ts
describe('MyComponent', () => {
let component: MyComponent;
let clickSpy: jasmine.Spy;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent]
TestBed.getTestingModule().inject(Scheduler)
を使用して、テスト内で独自のスケジューラを注入することができます。これにより、非同期処理のタイミングをより細かく制御することができます。
// component.spec.ts
import { Component, OnInit } from '@angular/core';
import { Scheduler } from 'rxjs';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent implements OnInit {
data: any;
constructor(private scheduler: Scheduler) { }
ngOnInit() {
this.scheduler.schedule(() => {
this.myService.getData().then(data => this.data = data);
});
}
}
// component.spec.ts
describe('MyComponent', () => {
let component: MyComponent;
let scheduler: Scheduler;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [
{ provide: Scheduler, useValue: jasmine.createSpyObj('Scheduler', ['schedule'])}
]
});
component = TestBed.createComponent(MyComponent);
scheduler = TestBed.inject(Scheduler);
});
it('should load data from service', async () => {
const mockData = { value: 1 };
scheduler.schedule.and.callFake((cb) => cb()); // スケジュールされたタスクを即座に実行
await component.fixture.detectChanges();
expect(component.data).toEqual(mockData);
});
});
Marble Testing
Marble Testingは、RxJSと組み合わせることで、非同期処理のテストをより詳細に記述することができます。テストケースでObservableのシーケンスを定義し、期待される結果を検証することができます。
この方法は、複雑な非同期処理をテストする場合に特に有用です。
FakeAsync and tick
Karma-Jasmineには、fakeAsync()
と tick()
関数を使用して、非同期処理をシミュレートすることができます。これらは、async
テストよりもシンプルな方法で非同期処理をテストしたい場合に役立ちます。
ngZone.run
ngZone.run()
関数は、Angular Change Detection Zone内でコードを実行するために使用されます。これは、非同期処理がコンポーネントのビューに反映されるようにする必要がある場合に役立ちます。
これらの方法はそれぞれ長所と短所があり、状況に応じて使い分けることが重要です。
上記以外にも、Angular 2 テストで非同期処理を扱うための様々なツールやライブラリが用意されています。
angular unit-testing karma-jasmine