Angular のコンポーネントのテスト
マジカルミライ2019を見てきました。初めてのライブでもあったので新鮮な体験でした。
前回と被ってそうですが、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 のテストは、 Jasmine
と karma
で動いています。
コード内の describe
、 it
や beforeEach
は、本題ではないので、ここでは詳しく説明しません(説明するほど知識がありません……)。
一応簡単にいうと、 describe
内にある it
関数が 1 単位のテストで、 beforeEach
は it
が呼ばれる前に行う初期化処理です(自動で呼ばれます)。
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
内で設定された TestBed
で AppComponent
のインスタンスを作成しています。作成したコンポーネントのインスタンスをそのまま返すわけではなく、 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 では、コンポーネントのテストを行うときは、以下のようになると思います。
- テスト対象のコンポーネントに必要なデータ(メタデータ)を
TestBed
に設定する - 設定した
TestBed
でコンポーネントを作成し、fixture を取得する。 - fixture を介して、コンポーネント、 View にアクセスし、期待する結果になっているか確認する。
上記を行うために主に必要な機能は以下です。
- TestBed.configureTestingModule(): TestBed を設定する
- TestBed.createComponent(AppComponent): 指定されたコンポーネントクラスのインスタンスを作成する
- ComponentFixture\
.detectChanges(): バインドしている部分を View 側に反映させる - fixture.debugElement.nativeElement: View の要素にアクセスし、期待する値になっているか確認する
今回 HTML 側を変更するとテスト側も変更する必要が出ました。これでは、気軽に HTML 側を変更するのが難しくなってしまいます。これを解決するには、PageObject
というパターンを使うことである程度解決することが出来ます。
次は、今回のテストをもう少し変更に強いテストに変えていきます。