- これは何か
- 前提知識
- Service Tests
- Component Test Basics
- Component Test Scenarios
- Component binding
- Component with external files
- Component with a dependency
- Component with async service
- Component marble tests
- Component with inputs and outputs
- Component inside a test host
- Routing component
- Routed components
- Nested component tests
- Components with RouterLink
- Use a page object
- Setup with module imports
- Override component providers
これは何か
Angular の公式ドキュメント(Angular - Testing)とテストに関する前提知識(用語、Jasmine使い方など)をまとめようとしたもの。
前提知識
Jasmine
spyOn(オブジェクト, 'メソッド名') // 本来の実装も実行 spyOn(obj, 'method').and.callThrough(); // 戻り値を変更 spyOn(obj, 'method').and.returnValue('stub'); // 処理を任意の関数に置き換える spyOn(obj, 'method').and.callFake(...) calls.count(); calls.first(); calls.mostRecent(); // 裸のスパイ関数 var spyFunction = jasmine.createSpy(); // 裸のスパイオブジェクト var spyObj = jasmine.createSpyObj('spyName', ['hoge', 'fuga', 'piyo']);
- Jasmine を使用したユニットテストで便利な「fdescribe・fit」「xdescribe・xit」 - Corredor
fdescribe
,fit
,xdescribe
,xit
用語
- テストダブル - Wikipedia
テストダブル (Test Double) とは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。ダブルは代役、影武者を意味する
- テストスタブ (テスト対象に「間接的な入力」を提供するために使う。)
- テストスパイ (テスト対象からの「間接的な出力」を検証するために使う。出力を記録しておくことで、テストコードの実行後に、値を取り出して検証できる。)
- モックオブジェクト (テスト対象からの「間接的な出力」を検証するために使う。テストコードの実行前に、あらかじめ期待する 結果を設定しておく。検証はオブジェクト内部で行われる。)
- フェイクオブジェクト (実際のオブジェクトに近い働きをするが、より単純な実装を使う。例として、実際のデータベースを置き換えるインメモリデータベースが挙げられる。)
- ダミーオブジェクト (テスト対象のメソッドがパラメータを必要としているが、そのパラメータが利用されない場合に渡すオブジェクト。
-
スタブ (stub) とは、コンピュータプログラムのモジュールをテストする際、そのモジュールが呼び出す下位モジュールの代わりに用いる代用品のこと。
- FakeとMockとStubの違い - kkamegawa’s weblog
Stub: Prevent the dependency from obstructing the method-under-test and torespond in a way that helps it proceed through its logical steps.
Mock: Allow the test code to verify that the code-under-test’s interaction with the dependency is proper, valid, and expected.
Service Tests
https://angular.io/guide/testing#service-tests
コンポーネントのテストは複雑になりがちなので、まずはサービスのテストから。
依存がない場合
service = new ValueService();
他のサービスに依存している場合
// 1. 実際のサービスをInject masterService = new MasterService(new ValueService()); // 2. FakeサービスをInject masterService = new MasterService(new FakeValueService()); // 3. Fake objectをInject const fake = { getValue: () => 'fake value' }; masterService = new MasterService(fake as ValueService); // 4. Spy serviceをInject const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValue']); const stubValue = 'stub value'; valueServiceSpy.getValue.and.returnValue(stubValue); masterService = new MasterService(valueServiceSpy);
実際のアプリケーションと同じように Angular の DI を利用する必要があるのであれば TestBed
を利用。
https://angular.io/guide/testing#angular-testbed
let service: ValueService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ValueService] }); }); it('should use ValueService', () => { service = TestBed.get(ValueService); expect(service.getValue()).toBe('real value'); });
Testing without beforeEach()
https://angular.io/guide/testing#testing-without-beforeeach
beforeEach
を必ずしも利用する必要はない
function setup() { const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValue']); const stubValue = 'stub value'; const masterService = new MasterService(valueServiceSpy); valueServiceSpy.getValue.and.returnValue(stubValue); return { masterService, stubValue, valueServiceSpy }; } it('#getValue should return stubbed value from a spy', () => { const { masterService, stubValue, valueServiceSpy } = setup(); // ... });
Testing HTTP services
https://angular.io/guide/testing#testing-http-services
HttpClient
のSpyをつくる
beforeEach(() => { httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); TestBed.configureTestingModule({ providers: [ ApiService, { provide: HttpClient, useValue: httpClientSpy }, ], }); }); // or beforeEach(() => { httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); service = new ApiService(<any>httpClientSpy); });
// 正常系 httpClientSpy.get.and.returnValue(asyncData(json)); // エラー系 httpClientSpy.get.and.returnValue(asyncError(errorResponse));
asyncData
,asyncError
Component Test Basics
https://angular.io/guide/testing#component-test-basics
大抵の場合、DOM関与なしでコンポーネントクラスだけをテストした方が、動作の多くを簡単に検証できる。実際、単純なコンポーネントならnew FooComponent()
のようにインスタンスを生成するだけでテストができる。
// component export class DashboardHeroComponent { @Input() hero: Hero; @Output() selected = new EventEmitter<Hero>(); click() { this.selected.emit(this.hero); } } // testing it('raises the selected event when clicked', () => { const comp = new DashboardHeroComponent(); const hero: Hero = { id: 42, name: 'Test' }; comp.hero = hero; comp.selected.subscribe(selectedHero => expect(selectedHero).toBe(hero)); comp.click(); });
依存関係がある場合はTestBedを利用する。
beforeEach(() => { TestBed.configureTestingModule({ // provide the component-under-test and dependent service providers: [ WelcomeComponent, { provide: UserService, useClass: MockUserService } ] }); // inject both the component and the dependent service. comp = TestBed.get(WelcomeComponent); userService = TestBed.get(UserService); }); // ... // 手動で ngOnInit it('should welcome logged in user after Angular calls ngOnInit', () => { comp.ngOnInit(); expect(comp.welcome).toContain(userService.user.name); });
Component DOM testing
https://angular.io/guide/testing#component-dom-testing
DOMのテストも必要な場合。コンポーネントを生成してテストする必要がある。
コンポーネントを生成するテストコード
describe('BannerComponent (minimal)', () => { it('should create', () => { TestBed.configureTestingModule({ declarations: [ BannerComponent ] }); const fixture = TestBed.createComponent(BannerComponent); const component = fixture.componentInstance; expect(component).toBeDefined(); }); });
- createComponent
- ComponentFixture
- 作成されたコンポーネントおよび対応する要素と対話するためのハーネス
- https://angular.io/api/core/testing/ComponentFixture
nativeElement
https://angular.io/guide/testing#nativeelement
DOMのAPIを利用したい場合。
const bannerElement: HTMLElement = fixture.nativeElement; const p = bannerElement.querySelector('p');
DebugElement
https://angular.io/guide/testing#debugelement
The DebugElement offers query methods that work for all supported platforms.
Component Test Scenarios
https://angular.io/guide/testing#component-test-scenarios
Component binding
https://angular.io/guide/testing#component-binding
- テンプレートにデータを反映させるには
detectChanges()
を実行させる必要がある - 自動で
fixture.detectChanges()
させる方法もある- https://angular.io/guide/testing#automatic-change-detection
{ provide: ComponentFixtureAutoDetect, useValue: true }
- https://angular.io/guide/testing#automatic-change-detection
Change an input value with dispatchEvent()
input elementの場合はdispatchEvent
も実行させる。
const nameInput: HTMLInputElement = hostElement.querySelector('input'); // ... // simulate user entering new name into the input box nameInput.value = inputName; // dispatch a DOM event so that Angular learns of input value change. nameInput.dispatchEvent(newEvent('input')); // Tell Angular to update the display binding through the title pipe fixture.detectChanges();
Component with external files
ng testなら不要
Component with a dependency
https://angular.io/guide/testing#component-with-a-dependency
依存関係があるコンポーネント
// providers: [ UserService ] // NO! Don't provide the real service! // Provide a test-double instead
Provide service test doubles
https://angular.io/guide/testing#provide-service-test-doubles
A component-under-test doesn't have to be injected with real services. In fact, it is usually better if they are test doubles (stubs, fakes, spies, or mocks).
let userServiceStub: Partial<UserService>; userServiceStub = { isLoggedIn: true, user: { name: 'Test User'} };
- Get injected services
// UserService actually injected into the component userService = fixture.debugElement.injector.get(UserService); // UserService from the root injector userService = TestBed.get(UserService);
- TestBed.get()
- Always get the service from an injector
- Final setup and tests
Component with async service
https://angular.io/guide/testing#component-with-async-service
Testing with a spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']); // Make the spy return a synchronous Observable with the test data getQuoteSpy = twainService.getQuote.and.returnValue( of(testQuote) );
Synchronous tests
https://angular.io/guide/testing#synchronous-tests
Async test with fakeAsync()
https://angular.io/guide/testing#async-test-with-fakeasync
fakeAsync(() => { /* test body */ })
- Angular - fakeAsync
The tick() function
Calling tick() simulates the passage of time until all pending asynchronous activities finish. In this case, it waits for the error handler's setTimeout();
Async observables
Async observable helpers
RxJS defer() returns an observable. It takes a factory function that returns either a promise or an observable. When something subscribes to defer's observable, it adds the subscriber to a new observable created with that factory.
RxJS defer() transform the Promise.resolve() into a new observable that, like HttpClient, emits once and completes. Subscribers will be unsubscribed after they receive the data value.
以下を用意
import { defer } from 'rxjs/observable/defer'; /** Create async observable that emits-once and completes * after a JS engine turn */ export function asyncData<T>(data: T) { return defer(() => Promise.resolve(data)); } /** Create async observable error that errors * after a JS engine turn */ export function asyncError<T>(errorObject: T) { return defer(() => Promise.reject(errorObject)); }
More async tests
Async test with async()
https://angular.io/guide/testing#async-test-with-async
fakeではないasync
whenStable
The test must wait for the getQuote() observable to emit the next quote. Instead of calling tick(), it calls fixture.whenStable().
tickの代わりにfixture.whenStableを実行する必要がある
Jasmine done()
https://angular.io/guide/testing#jasmine-done
もちろん一般的な done も使える
But it is occasionally necessary. For example, you can't call async or fakeAsync when testing code that involves the intervalTimer() or the RxJS delay() operator.
Component marble tests
https://angular.io/guide/testing#component-marble-tests
Component with inputs and outputs
https://angular.io/guide/testing#component-with-inputs-and-outputs
@Input に対応
// ... comp = fixture.componentInstance; // ... // mock the hero supplied by the parent component expectedHero = { id: 42, name: 'Test Name' }; // simulate the parent setting the input property with that hero comp.hero = expectedHero; // trigger initial data binding fixture.detectChanges();
Clicking
https://angular.io/guide/testing#clicking
- click() helper
- 対象が
HTMLElement
ならclick()
メソッドを利用(通常はこっちでおk) HTMLElement
以外ならちょっと工夫が必要
- 対象が
Component inside a test host
https://angular.io/guide/testing#component-inside-a-test-host
Component with inputs and outputs
と違うやり方。ホストのコンポーネントから操作する。TestHostComponent
を作ってその中で制御
Routing component
https://angular.io/guide/testing#routing-component
- Routerをもっているコンポーネントのテスト
- 「Routerのテスト」をする必要はないのでspyをつくる
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']); // ... const spy = router.navigateByUrl as jasmine.Spy; const navArgs = spy.calls.first().args[0];
Routed components
https://angular.io/guide/testing#routed-components
- Router Navigation の遷移先
- ActivatedRoute をどうテストするか
paramMap returns an Observable that can emit more than one value during a test. You need the router helper function, convertToParamMap(), to create a ParamMap. Other routed components tests need a test double for ActivatedRoute.
Nested component tests
https://angular.io/guide/testing#nested-component-tests
不要なコンポーネントをどう扱うか。
Stubbing unneeded components
@Component({selector: 'app-banner', template: ''}) class BannerStubComponent {} TestBed.configureTestingModule({ declarations: [ AppComponent, BannerStubComponent ] });
NO_ERRORS_SCHEMA
tells the Angular compiler to ignore unrecognized elements and attributes.
Use both techniques together
実際には両方のテクニックを利用するはず
Components with RouterLink
https://angular.io/guide/testing#components-with-routerlink
RouterLinkのスタブを作成
@Directive({ selector: '[routerLink]', host: { '(click)': 'onClick()' } }) export class RouterLinkDirectiveStub { @Input('routerLink') linkParams: any; navigatedTo: any = null; onClick() { this.navigatedTo = this.linkParams; } }
By.directive and injected directives
beforeEach(() => { fixture.detectChanges(); // trigger initial data binding // find DebugElements with an attached RouterLinkStubDirective linkDes = fixture.debugElement .queryAll(By.directive(RouterLinkDirectiveStub)); // get attached link directive instances // using each DebugElement's injector routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub)); });
Use a page object
Setup with module imports
https://angular.io/guide/testing#setup-with-module-imports
モジュールインポートについて解説
One approach is to configure the testing module from the individual pieces as in this example:
- Import a shared module でも可能
- 実は自身を含む Feature module を読み込んでもおk
Override component providers
https://angular.io/guide/testing#override-component-providers
どうやってコンポーネントのproviders
で登録されたサービスをおきかえるか
(すでに中途半端な内容になっていますが、一旦ここまで。ここからさらに、DirectiveやPipeのテストの説明、よくある質問などがAngular - Testingには記述されています。)