お茶漬けびより

"あなたに教わったことを、噛んでいるのですよ" 五等分の花嫁 7巻 「最後の試験が五月の場合」より

Angular のコンポーネントのテスト

f:id:pickles-ochazuke:20190812094957j:plain

マジカルミライ2019を見てきました。初めてのライブでもあったので新鮮な体験でした。

f:id:pickles-ochazuke:20190812094934j:plain

前回と被ってそうですが、Angular のコンポーネントのテストの入門的な話です。

コンポーネントのテスト

Angular のテストは以下のコマンドで行います

ng test

ng new コマンドでプロジェクトを作成すると app ディレクトリ の直下に app.component.html, app.component.spec.ts, app.component.ts などが作成されます。 今回、テストを行うために必要なファイルは先に挙げた3つなので、これだけを編集していきます。

自分の環境では、HTML ファイル(app.component.html)を pug ファイル(app.component.pug)にしていますが、今回は特に難しいことはしていないので、知らなくても問題ないと思っています(一応、 HTML に書き換えた内容も載せますが、表示の確認をしていないので間違いがあるかもしれません)

今回テストする HTML(app.component.pug) の内容は以下になります。

span#title {{title}}

span タグに title という ID がついており、コンポーネントtitle という変数をバインドしています。 HTML だと以下のようになります。

<span id="title"> {{title}} </span>

コンポーネント側(app.component.ts)は以下

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.pug',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'angular-try';
}

これをテストしていきます。

テストをするには、テストコードが必要ですが、Angular では、テストコードはコンポーネントを作ったときに一緒に作成されます(app.component.spec.ts)。 最初は以下のようになっています(流し見する程度でいいです)。

このテストを実行するといくつか失敗します。コンポーネント側と View 側を変更しているためです。

import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have as title 'angular-try'`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('angular-try');
  });

  it('should render title in a h1 tag', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-try!');
  });
});

Angular のテストは、 Jasminekarma で動いています。

コード内の describeitbeforeEach は、本題ではないので、ここでは詳しく説明しません(説明するほど知識がありません……)。

一応簡単にいうと、 describe 内にある it 関数が 1 単位のテストで、 beforeEachit が呼ばれる前に行う初期化処理です(自動で呼ばれます)。

TestBed

beforeEach では、以下のような処理が呼ばれています。

TestBed.configureTestingModule({
  declarations: [
    AppComponent
  ],
}).compileComponents();

TestBed は、 @NgModuleエミュレータです。そのコンポーネントを作成するために必要なモジュールやプロパティを用意して、 configureTestingModule で読み込ませてからコンポーネントを作る必要があります。 compileComponents コンパイルをする処理ですが、CLI で実行している場合、コンパイルはしてくれているので通常必要ありません。

今回は特別な機能を使っていないのでデフォルトのままでいいです。

TestBed は Angular のテストの中でも最も重要な機能です。ここでは、全部紹介しきれませんが、一度調べてみることをオススメします。

AppComponent の作成

次は、テストを一つずつ見ていきます。 以下は、 AppComponent が正しく作成できているか確認しているテストです。

it('should create the app', () => {
  const fixture = TestBed.createComponent(AppComponent);
  const app = fixture.debugElement.componentInstance;
  expect(app).toBeTruthy();
});

TestBed.createComponent(AppComponent) では、 beforeEach 内で設定された TestBedAppComponentインスタンスを作成しています。作成したコンポーネントインスタンスをそのまま返すわけではなく、 ComponentFixture という型のインスタンスを返します。コンポーネントには、この fixture を介して操作を行います。

デフォルトでは、 fixture.debugElement.componentInstanceコンポーネントにアクセスしていますが、 fixture からコンポーネントインスタンスが取得できるため、 debugElement はなくても問題ありません。

AppComponent のテスト

2 つ目のテストを見てみると、コンポーネントにアクセスしている様子がわかります。

it(`should have as title 'angular-try'`, () => {
  const fixture = TestBed.createComponent(AppComponent);
  const app = fixture.debugElement.componentInstance;
  expect(app.title).toEqual('angular-try');
});

appコンポーネントインスタンスです。コンポーネントが持つ変数が期待通りの値になっているか確認しています。

View 側のテスト

3 つ目のテストでは、コンポーネントと紐付いている HTML 要素のテストをしています。また、このテストは失敗していると思います。

it('should render title in a h1 tag', () => {
  const fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();
  const compiled = fixture.debugElement.nativeElement;
  expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-try!');
});

View 側をテストしたい場合は、querySelector を使ってエレメントを取り出し、期待の結果になっているか確認ができます。

実際の Angular では、変更を検知して結果を View に反映してくれますが、テストの場合は手動で変更を伝える必要があります。それが fixture.detectChanges() です。

これを実行しない場合、バインドされている部分が変化せずテストが失敗します。

テストを通す

では、最後のテストが失敗していると思いますので、このテストを通していきます。

まず、テストを確認します。

it('should render title in a h1 tag', () => {
  const fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();
  const compiled = fixture.debugElement.nativeElement;
  expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-try!');
});

コンポーネントを作成するところは、別のテストで問題なく通っているので、最後のテストしている部分を見ます。

現在の HTML は以下のようになっています。

span#title {{title}}

h1 のタグは存在せず、 span タグになっています。また、表示する文字列は、title 変数の内容だけなので、 angular-try になるはずです。テストを以下のように書き換えます。

expect(compiled.querySelector('span').textContent).toContain('angular-try');

これはテストが通るはずです。最後に、テストの名称を変更します。変更したテストは以下のようになります。

it('should render title in a span tag', () => {
  const fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();
  const compiled = fixture.debugElement.nativeElement;
  expect(compiled.querySelector('span').textContent).toContain('angular-try');
});

まとめ

以上、コンポーネントのテストを簡単に説明しました。Angular では、コンポーネントのテストを行うときは、以下のようになると思います。

  1. テスト対象のコンポーネントに必要なデータ(メタデータ)を TestBed に設定する
  2. 設定した TestBedコンポーネントを作成し、fixture を取得する。
  3. fixture を介して、コンポーネント、 View にアクセスし、期待する結果になっているか確認する。

上記を行うために主に必要な機能は以下です。

  • TestBed.configureTestingModule(): TestBed を設定する
  • TestBed.createComponent(AppComponent): 指定されたコンポーネントクラスのインスタンスを作成する
  • ComponentFixture\.detectChanges(): バインドしている部分を View 側に反映させる
  • fixture.debugElement.nativeElement: View の要素にアクセスし、期待する値になっているか確認する

今回 HTML 側を変更するとテスト側も変更する必要が出ました。これでは、気軽に HTML 側を変更するのが難しくなってしまいます。これを解決するには、PageObject というパターンを使うことである程度解決することが出来ます。

次は、今回のテストをもう少し変更に強いテストに変えていきます。