とんかつ時々あんどーなつ

〜たとえ低空でも飛行していられるように〜

ComponentHarness を使って Angular アプリケーションの単体テストを書く

ComponentHarness を使って Angular アプリケーションの単体テストを書いてみた

試したコード

github.com

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 を使った単体テストを書く

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 もいっぱいあるからもう少し素振りは必要そうだなぁ

参考