Cover image for Angular2 Unit Testing - DOMのテスト
angularangular2karmajasmineunit test

Angular2 Unit Testing - DOMのテスト

March 16, 2016

7 min read

mitsuruogMitsuru Ogawa

Angular2 の実装の方法は記事をよく目にする機会が増えたので、テストについての自分が困らないように調べてみたシリーズ。

今回は DOM が関連するテスト。

(注意)Angular 2.0.0-beta.9 をベースに話しています。 E2E テストは protractor がそのまま利用できると思うので、ここでのテストはユニットテストの話です。

Angular2 Unit Testing

  1. 準備
  2. 基本
  3. Mock, Spy の基本(TBD)
  4. DOM のテスト
  5. XHR のテスト
  6. Component のテスト(TBD)
  7. Service のテスト(TBD)
  8. Pipe のテスト
  9. Directive のテスト(TBD)
  10. カバレッジ

DOM のテスト

DOM のテストについて紹介します。

DOM のような状態が不安定なものをテストする場合、テスト対象の状態を一定に固定するため、fixtureと呼ばれる土台(仮の DOM)を利用してきました。 Angular2 の DOM のテストでも、これまでと同様に fixture を利用します。

fixture を準備する(TestComponentBuilder)

まず、テストで利用する fixture を準備します。

Angular2 にはテスト用 fixture を作成するための APITestComponentBuilderが準備されています。 TestComponentBuilderは、fixture 用の Component を作成して外側から操作・検証する API を提供します。

DOM のテストのおおまかな流れは次のような形です。

import {
  it,
  describe,
  expect,
  inject,
  injectAsync,
  beforeEach,
  beforeEachProviders,
  TestComponentBuilder
} from 'angular2/testing';
import {Component} from 'angular2/core';

describe('DOMのテスト', () => {

  beforeEachProviders(() => [
    TestComponentBuilder
  ]);

  it('なにかのテスト', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    // ここに実際のテストコードを書く
  }));

});

// fixture用Component
@Component({ selector: 'container' })
class TestComponent { }

TestComponentBuilder@Injectable指定されているため、他の Provider と同様にbeforeEachProvidersで呼び出した後に、injectAsyncでテストコード中に DI して利用する必要があります。

続いて、テストで利用する使い捨て Component を作成します。これをテスト用 fixture として利用します。

fixture を作成する(overrideTemplate, createAsync)

続いて実際に fixture を作成します。

テスト用 fixture が準備できたので、この Component をテンプレートを上書きして DOM を作成します。

// itの部分だけ抜粋
it(
  "なにかのテスト",
  injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    let template = "<div>Hello Angular2 :)</div>";
    return tcb
      .overrideTemplate(TestComponent, template)
      .createAsync(TestComponent)
      .then((fixture) => {
        // ここに検証コードを書く
      });
  })
);

overrideTemplateに Component と上書きするテンプレートを渡して、createAsyncを呼び出すと、実際にテストする fixture が作成されます。

実際には、テンプレートに Component や Directive を含めることが多いかと思います。 Angular1 での$compile(template)($rootScope)とほぼ同じものだと考えて大丈夫です。

TestComponentBuilderで作成された fixture はComponentFixtureクラスなり、次のようなテストで利用する API が準備されています。

  • debugElement
    • テスト用の Helper クラス
  • componentInstance
    • TestComponent のインスタンス
  • nativeElement
    • テスト用 fixture の HTMLElement を表す
  • detectChanges()
    • テスト中に Component を変更するために、Component の変更検知サイクルを発火します。

よく利用するものについて紹介します。

fixture を検証する API(nativeElement)

nativeElementはテスト用 fixture のHTMLElementを返す API です。 HTMLElementは HTML 標準の API であるため、これを利用してテスト用 fixture を検証・操作します。

HTMLElement - Web APIs | MDN

// itの部分だけ抜粋
it(
  "なにかのテスト",
  injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
    let template = "<div>Hello Angular2 :)</div>";
    return tcb
      .overrideTemplate(TestComponent, template)
      .createAsync(TestComponent)
      .then((fixture) => {
        let div = fixture.nativeElement.querySelector("div");
        // HTMLElementを検証する
        expect(div).toHaveText("Hello Angular2 :)");
      });
  })
);

fixture を変更する(detectChanges)

detectChanges()は、Angular1 のscope.$digest()に似ており、強制的に Angular へ変更を検知させる仕組みです。

Angular2 内部では Angular1 のdirty checkingに代わる独自の変更検知サイクルを持っています。 テストで Click イベントなどの非同期を利用する場合、この変更検知サイクルを発火して Angular2 に変更を検知させる必要があります。

Angular2 の変更検知サイクルについてはこの記事が詳しいです。

次のテストコードは、マウスが当たると文字色が red に変わる Component のテストだと仮定します。

// ... describeの部分だけ抜粋
describe("DOMのテスト", () => {
  let mouseenter;

  beforeEachProviders(() => [TestComponentBuilder]);

  beforeEach(() => {
    // mouseenterイベントを定義します
    mouseenter = new MouseEvent("mouseenter", {
      view: window,
      bubbles: true,
      cancelable: true,
    });
  });

  it(
    "mouseenterするとredに変わること",
    injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
      let template = "<div>Hello Angular2 :)</div>";
      return tcb
        .overrideTemplate(TestComponent, template)
        .createAsync(TestComponent)
        .then((fixture) => {
          let div = fixture.nativeElement.querySelector("div");

          // mouseenterイベントを強制的に起こします
          div.dispatchEvent(mouseenter);

          // detectChangesしないと変更が検知されない
          expect(div.style.backgroundColor).toEqual("red"); // => NG
          fixture.detectChanges();
          expect(div.style.backgroundColor).toEqual("red"); // => OK
        });
    })
  );
});

まとめ

DOM についてのテストは以上です。

DOM が関連するテストはやはり面倒ですね。メンバーの習熟度や学習コストを鑑みて、自動テストをしないという選択肢もありだと思います。 テストコードの全体像は、こちらで雰囲気をつかめると思います。

_angular2-attribute-directive/highlight.directive.spec.ts at master · mitsuruog/_angular2-attribute-directive

今回の例では、検証に fixture のnativeElementを利用していましたが、debugElementについてはまだ利用用途があまりはっきりとわかっていません。 機会があれば、もう少し掘り下げようかと思います。

PR

こちらに初学者のための Minimum starter kit を作成しましたので、ぜひ利用してください。

mitsuruog/angular2-minimum-starter: Minimum starter kit for angular2