ComponentHarness を使って Angular アプリケーションの単体テストを書いてみた
試したコード
ComponentHarness とは
- angular/components v9.0.0 に追加された testing tool
- テストコードを書く時の役割が ComponentHarness を作る人と ComponentHarness を使ってテストコードを書く人に分けることが可能
- 人を分けるわけではなく考えるレイヤーを分ける
- ComponentHarness を使うと ComponentHarness の層で DOM 構造などを隠蔽できるようになるため実際のテストコードでは DOM の変更に強くなるなどの利点がある
パッケージインストール
- angular/cdk/testing が提供しているので cdk の v9 以上が必要
$ yarn ng add @angular/cdk
テスト対象のアプリケーション
- HeaderComponent を用意して AppComponent に配置しただけのアプリケーション
app.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'app-root', template: ` <app-header></app-header> `, }) export class AppComponent {}
header.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'app-header', template: ` <header class="header"> <h1 class="title">Header Title</h1> </header> `, }) export class HeaderComponent {}
Custom Harness を作る
- angular/components が提供する component は ComponentHarness も提供されているが自作の component は Custom Harness を作る必要がある
header-harness.ts
import { ComponentHarness } from '@angular/cdk/testing'; export class HeaderHarness extends ComponentHarness { static hostSelector = 'app-header'; protected _getTitleElement = this.locatorFor('h1'); async getTitleText() { const titleElement = await this._getTitleElement(); return titleElement.text(); } }
- Harness は angular/cdk/testing が提供する ComponentHarness を継承して class を作る
- Custom Harness は static な
hostSelector
を持つ必要がある- 基本的にこの
hostSelector
は対象 component のselector
と同じにしておけば問題ない
- 基本的にこの
- ComponentHarness は DOM 要素を見つけるための API を持っていて、今回は HeaderComponent の h1 要素を見つけるために
locatorFor()
を使って_getTitleElement
を定義している- 古い要素のキャッシュを防ぐために
locatorFor()
は関数を返す仕組みになっている。そのため_getTitleElement()
を呼び出すための wrapper method を実装する必要がある - ComponentHarness が提供する locator method は
locatorFor()
の他にもlocatorForOptional()
やlocatorForAll()
がある
- 古い要素のキャッシュを防ぐために
Harness を使った単体テストを書く
- 上で作った HeaderHarness を使って単体テストを書く
app.component.spec.ts
- HeaderComponent は AppComponent に配置されているので app.component.spec.ts で HeaderHarness を使いテストする
import { HarnessLoader } from '@angular/cdk/testing'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { HeaderComponent } from './header/header.component'; import { HeaderHarness } from './header/testing/header-harness'; describe('AppComponent', () => { let fixture: ComponentFixture<AppComponent>; let loader: HarnessLoader; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [AppComponent, HeaderComponent], }).compileComponents(); fixture = TestBed.createComponent(AppComponent); loader = TestbedHarnessEnvironment.loader(fixture); })); it(`should have the HeaderComponent`, async () => { const headerHarness = await loader.getHarness(HeaderHarness); expect(headerHarness).toBeTruthy(); }); });
- HarnessLoader インスタンスを取得するために
TestbedHarnessEnvironment.loader()
を使用する- fixture 内にある Harness を取得するために必要なので今回は HeaderComponent を持つ AppComponent を fixture として指定
- HarnessLoader インスタンスの
getHarness()
で対象の Harness(今回は HeaderHarness) を指定する - fixture で指定した AppComponent 内に HeaderComponent を表示していれば
getHarness(HeaderHarness)
で値を取得できるので、それが truthy であれば HeaderComponent が表示できていることになる
header.component.spec.ts
- HeaderComponent 自身で HeaderHarness を使いテストする
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { HeaderComponent } from './header.component'; import { HeaderHarness } from './testing/header-harness'; describe('HeaderComponent', () => { let fixture: ComponentFixture<HeaderComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [HeaderComponent], }).compileComponents(); fixture = TestBed.createComponent(HeaderComponent); })); it('Heading text が Header Title であること', async () => { const headerHarness = await TestbedHarnessEnvironment.harnessForFixture(fixture, HeaderHarness); const titleText = await headerHarness.getTitleText(); expect(titleText).toBe('Header Title'); }); });
- 対象の fixture 自身の Harness を用意するときは
TestbedHarnessEnvironment.harnessForFixture()
を使用する - HeaderHarness のインスタンスで用意したメソッド
getTitleText()
を呼び出すと titleText を取り出せ、そこの文字列の一致をテストすることができる
感想
最低限動かすだけでもそれなりに覚えることはある印象。。
が、ComponentHarness を作るレイヤーと ComponentHarness を使ってテストコードを書くレイヤーを分けられるのは確かにメリットだと思う。
今回の例でも header のタイトルを表示している要素を h1 から h2 に変えても Harness の変更は必要だが、それ使うテストコードの変更は不要だとわかる。
拾いきれなかった API もいっぱいあるからもう少し素振りは必要そうだなぁ