【技術記事】Angular4(Angular2~)のユニットテスト【Angularのユニットテストの基本とComponentの簡単なテスト編】

Angular4(Angular2~)のユニットテスト【Angularのユニットテストの基本とComponentの簡単なテスト編】

Angularのユニットテストを勉強中なので、何回かに分けて記事にする。
今回の記事は、以下の公式ドキュメントの4つのセクションをかなり参考にさせていただいている。
Introduction to Angular testing The first karma test Test a component Test a component with an external template

今回は、Angularのテストの大まかな流れと、簡単なComponentのテストを記事の対象とする。

使うテストツール

Angularのテストでは、JavaScript界のいくつかのテストツールを使用する。それらのツールを理解していないと挫折するので、まずは、Angular自体のテストに先立って、それらのツールを学ぶ必要がある。以下を参考にするとわかりやすい。

jasmine

JavaScript 向けテスティングフレームワーク

jasmine公式 https://jasmine.github.io/2.0/introduction.html
jasmineのわかりやすい記事
http://qiita.com/opengl-8080/items/cf3acafda9756f4b04c9 http://qiita.com/hmsk/items/8f6965968692186b1ea1

karma

ブラウザでユニットテストを実行するためのテストランナー

karmaのわかりやすい記事
http://qiita.com/howdy39/items/b9d704e7f84053924da3 karma公式
https://karma-runner.github.io/1.0/index.html

被テストComponent

app.component.ts

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'テストだよ';

  /**
   * h1の文章を変更する
   */
  changeH1Element() {
    this.title = 'クリックされたぜ!';
  }
}

app.component.html

<h1>
  {{title}}
</h1>

<p class="button" (click)="changeH1Element()">ここをクリックして</p>

タイトルがあって、「ここをクリックして」というところを押すと、タイトルが変更される。
なお、今回の目的はAngularのユニットテストの学習のため、CSSなどによる装飾等は一切行わない。

テストクラス

app.component.spec.ts

import {ComponentFixture, TestBed, async, ComponentFixtureAutoDetect, fakeAsync, tick} from '@angular/core/testing';
import {By}              from '@angular/platform-browser';
import {DebugElement}    from '@angular/core';
import {AppComponent} from './app.component';


// describeでテストSuiteを作成
describe('AppComponentのテスト', () => {
  // テストの中のAppComponent
  let comp: AppComponent;
  // ComponentFixtureは、 componentのインスタンスそのものとcomponentのDOM elementのハンドルであるDebugElementへのアクセスを提供する。
  let fixture: ComponentFixture<AppComponent>;
  // ComponentのDOM elementのhandle
  let de: DebugElement;
  let el: HTMLElement;


  // 各Spec(個々のテスト)が開始される前に行う処理を設定する。
  // 非同期処理
  // Componentのインスタンスを生成する前に、Angular template compilerが外部ファイルを読み込む
  beforeEach(async(() => {
    // テストしたいクラスのためのモジュール環境をconfigureTestingModuleメソッドで設定する。
    // メタデータの登録
    TestBed.configureTestingModule({
      // テストされるComponentを登録
      declarations: [
        AppComponent
      ],
      providers: [
        {provide: ComponentFixtureAutoDetect, useValue: true}
      ]
    }).compileComponents(); // 外部ファイルをコンパイル

  }));

  // 同期処理
  // Componentのインスタンスを生成
  beforeEach(() => {
    // ComponentFixtureは、 componentのインスタンスそのものとcomponentのDOM elementのハンドルであるDebugElementへのアクセスを提供する。
    fixture = TestBed.createComponent(AppComponent);
    // テストされるComponentのインスタンス
    comp = fixture.componentInstance; 

    // queryは、fixtureのDOM全体の中から、引数で与えたelementを満たす最初のDom elementとマッチしたDebugElementを返す
    // Byでは、cssのselectorを生成している。
    de = fixture.debugElement.query(By.css('h1'));

    el = de.nativeElement;
  });


  // itでSpecを作成
  it('AppComponentのインスタンスが生成できているかどうか', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    // trueかどうか
    expect(app).toBeTruthy();
  }));
  it('何もしない場合のタイトルがAppComponentのtitleと同じかどうか', async(() => {
    expect(el.textContent).toContain(comp.title);
  }));
  it('何もしない場合のタイトルがAppComponentのtitleと同じかどうか(上のテストと同じことをしている)', async(() => {
    expect(el.textContent).toContain('テストだよ');
  }));

  it('detectChangesが1回起きた時、h1の値が変更されるかどうか', async(() => {
    comp.title = 'クリックされたぜ!';
    fixture.detectChanges();
    expect(el.textContent).toContain(comp.title);
  }));

  it('changeH1Elementが呼び出されたら、titleが変更される', async(()=> {
    comp.changeH1Element();
    expect(comp.title).toBe('クリックされたぜ!');
  }));
});

各々の処理で行っていることは、コード中にコメントとして記述してある。

Angularのテストの大まかな構成

基本的には、jasmineの構成に則っているようである。
1. beforeEachで、各テストSpecの処理の前に行う処理を記述する。(場合によってはafterEachで、各テストSpecの処理の後に行う処理を記述する。)
2. descibeでSuite(テストの塊のようなもの)を作成し、その中でitでSpec(個々のテストメソッド)を複数記述する。このitの中で、様々なテストを行う。

Angularのテストで使用するクラスとメソッド

TestBed

公式APIリファレンス
https://angular.io/docs/ts/latest/api/core/testing/index/TestBed-class.html

Configures and initializes environment for unit testing and provides methods for creating components and services in unit tests. 引用元:https://angular.io/docs/ts/latest/api/core/testing/index/TestBed-class.html

(意訳) ユニットテスト用の設定と環境の初期化を行う。
また、ユニットテスト内のcomponentとserviceを生成するためのメソッドを作成する。

It creates an Angular testing module—an @NgModule class—that you configure with the configureTestingModule method to produce the module environment for the class you want to test. 引用元: https://angular.io/docs/ts/latest/guide/testing.html#!%23q-spec-file-location

(意訳) TestBedは、Angularのテスティングモジュール(@NgModuleクラス)を生成する。
@NgModuleクラスはテストしたいクラスのためのモジュール環境をconfigureTestingModuleメソッドで設定する。

=> テストの環境初期化と設定を行う。

TestBed.configureTestingModule

テストしたいクラスのためのモジュール環境を設定する。
メタデータの登録を行う。@NgModuleのテスト版のようなもの

beforeEach (jasmineのメソッド)

各Spec(個々のテスト)が開始される前に行う処理を設定する。

TestBed.createComponentとComponentFixture

TestBed.createComponent 公式APIリファレンス
https://angular.io/docs/js/latest/api/core/testing/index/TestBed-class.html
ComponentFixture 公式APIリファレンス
https://angular.io/docs/ts/latest/api/core/testing/index/ComponentFixture-class.html

The createComponent method returns a ComponentFixture, a handle on the test environment surrounding the created component. The fixture provides access to the component instance itself and to the DebugElement, which is a handle on the component's DOM element.
引用元: https://angular.io/docs/ts/latest/guide/testing.html#!%23component-fixture

(意訳) createComponent(TestBed.createComponent)は、生成されたcomponentのテスト環境のハンドルであるComponentFixtureクラスを返す。ComponentFixtureは、 componentのインスタンスそのものとcomponentのDOM elementのハンドルであるDebugElementへのアクセスを提供する。

=> ComponentFixture = テストしたいComponentのインスタンスとそのComponentのDebugElementへのアクセスを提供するもの。
しつこいようだが、これによってテスト対象のComponentのインスタンスとDOM elementを操作できるようになる。

TestBed.createComponentは、TestBedインスタンスの設定を閉じるので、createComponentを使用した後にTestBedの設定系のメソッドを使用してはいけないようである。

DebugElement.query

公式APIリファレンス
https://angular.io/docs/ts/latest/api/core/index/DebugElement-class.html

queryは、fixtureのDOM全体の中から、引数で与えたelementを満たす最初のDom elementとマッチしたDebugElementを返す。
fixture.debugElement.queryのDebugElementと戻り値のDebugElementは異なる。

queryAllの場合は、引数で与えたelementを満たす全てのDom elementの配列を返す。

参考 https://angular.io/docs/ts/latest/guide/testing.html#!%23component-fixture
https://angular.io/docs/js/latest/api/core/index/DebugElement-class.html#!%23query-anchor

By

AngularのUtilityで、 predicates(述語と単純に訳していいのか不明)を生成する。

公式APIリファレンス https://angular.io/docs/ts/latest/api/platform-browser/index/By-class.html

detectChanges

componentのchange detection cycleを行う。

公式APIリファレンス https://angular.io/docs/ts/latest/api/core/testing/index/ComponentFixture-class.html#!%23detectChanges-anchor detectChangesについては、以下がわかりやすい。
http://qiita.com/laco0416/items/523d96ddbfe55c4e6949

以下の記事の言葉をお借りすれば、

change Detectionとはモデルの変更を検知し、UIに反映することである

とのこと
引用元 http://qiita.com/laco0416/items/523d96ddbfe55c4e6949

fixture.detectChanges()を呼ぶことで、テストからAngilarにchange detection が行われたことを伝える。

Angularで外部のtemplateを読み込む

外部のtemplateやcssファイルを使用するときは、ちょっとした処理が必要なようだ。

But the Angular template compiler must read the external files from the file system before it can create a component instance. That's an asynchronous activity.

引用元: https://angular.io/docs/ts/latest/guide/testing.html#!%23component-fixture

(意訳)
Angularでは、Componentのインスタンスを生成する前に、Angular template compilerが外部ファイルを読み込む必要があり、これは非同期な活動である。

=>
つまり、Componentのインスタンスを生成する前にAngular template compilerが外部ファイルを読み込めるような処理をbeforeEachの中で行う必要なあるわけである。

TestBed.compileComponents

公式APIリファレンス

まず、テスティングモジュールで設定されたComponentを非同期でコンパイルする。
上記のcompileComponentsが完了したら、外部のファイルや、cssファイルはinlinedされていて、
TestBed.createComponentは、同期的にComponentのインスタンスを生成することができる。

参考: https://angular.io/docs/ts/latest/guide/testing.html#!%23component-fixture

templateやcssを外部ファイルにするときは、必ず「Componentのインスタンスを生成する」前に「外部ファイルを読み込む」ように非同期、同期をうまく使う。(コードの記述はこの順番でなくても構わない)

テスト実行画面

以下のコマンドをコマンドライン上で打つとテストランナーであるkarmaによってブラウザが立ち上がり、テスト結果がブラウザ上に表示される。

ng test

ng-karma.png

参考にさせていただいたサイト

公式 https://angular.io/docs/ts/latest/guide/testing.html#!%23component-fixture

jasmine公式
https://jasmine.github.io/2.0/introduction.html jasmineわかりやすい記事
http://qiita.com/opengl-8080/items/cf3acafda9756f4b04c9 http://qiita.com/hmsk/items/8f6965968692186b1ea1

karmaのわかりやすい記事
http://qiita.com/howdy39/items/b9d704e7f84053924da3

karma公式
https://karma-runner.github.io/1.0/index.html

公式APIリファレンス
https://angular.io/docs/ts/latest/api/core/testing/index/TestBed-class.html https://angular.io/docs/js/latest/api/core/testing/index/TestBed-class.html https://angular.io/docs/ts/latest/api/core/testing/index/ComponentFixture-class.html https://angular.io/docs/ts/latest/api/core/index/DebugElement-class.html https://angular.io/docs/ts/latest/api/platform-browser/index/By-class.html https://angular.io/docs/ts/latest/api/core/testing/index/ComponentFixture-class.html#!%23detectChanges-anchor

detectChangesについて
http://qiita.com/laco0416/items/523d96ddbfe55c4e6949

※ Qiitaでも同一記事を投稿している。

qiita.com