はじめに
Angularの単体テストって「デフォルトでフレームワークとテストランナー(Jasmine, karma)が入っているからやりやすい!」
「テストの実行が簡単」
と言われていることが多い気がするのですが、テスト自体を書くのはかなり難易度が高いように感じました。
単純に自前で作った関数の実行だけだったらともかく、アプリケーション全体としてテストを書いていこうとしたら、初見では未知なことが非常に多い印象でした。
さらに困ったことに、各種テスト(コンポーネントのテスト、UI操作イベントのテスト、APIリクエスト)の書き方がまとまった形で書かれているドキュメントなどにも出会えなかったため、毎度毎度テストケースを追加するたびににググってstackoverflowにたどり着いてどうにか書いて、と繰り返す羽目になっていました。
このへんがだいぶ面倒だったので、フロントエンドのアプリケーションでよくありそうな実装に対して、単体テストの書き方をここにまとめてみます。
前提
フロントエンドのアプリケーションは以下のようなものです。 - Angular 9 - Jasmine/Karma (Angular CLIでのインストール時のデフォルト) - UIフレームワーク: angular material各種テストケース
目次 - コンポーネントが作成されることのテスト - コンポーネント内の要素のテスト - コンポーネント間のデータ受け渡しのテスト - イベント発火のテスト初期状態
コンポーネントをコマンドラインで作成した時に自動的に作られるテストファイル(spec.ts)は以下のようなものですimport { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HogeComponent } from './hoge.component';
describe('HogeComponent', () => {
let component: HogeComponent;
let fixture: ComponentFixture<HogeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HogeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HogeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
コンポーネントが作成されることのテスト
デフォルトの `'should create'`で、コンポーネントが作成されること自体はテストされます。ここはこのままでもいいのかと思います。コンポーネント内の要素のテスト
デフォルトで作成されるコンポーネントのHTML要素はを持つのみです。
<p>hoge works!</p>
このようにstaticなデータを持つ場合には、 p要素が"hoge works!"であることというのがテストとして考えられます。
その場合には以下のようなテストケースを追加することになります
...
import { By } from '@angular/platform-browser'; // importの追加
...
it('p要素のテキストがhoge works!であること', () => {
const el = fixture.debugElement.query(By.css('p')).nativeElement; //p要素を nativeElementとして取得
expect(el.textContent).toBe('hoge works!'); // 取得した要素のtextContentと期待値の比較
});
要素の取得方法は基本的にはfixture.debugElement.query(By.css('要素取得のクエリ'))
となります。
nativeElementで取得すればDOM要素として取得できるので、テキストだったり、要素のdisabled状態だったりを取得することができます。
By.css
での要素取得は、タグ名ならtagName
クラスなら.class-name
、複数要素の取得の場合にはquery()
の代わりにqueryAll()
を使う、など様々な方法があります。
コンポーネント間のデータ受け渡しのテスト
親子間で要素の受け渡し、イベント発火には、`@Input`, `@Output`が使われます。最小のコードは以下のようなものです。<p>hoge works!</p>
<span>{{text}}</span>
<button (click)="clickButton()">Button</button>
export class HogeComponent implements OnInit {
@Input() text: string // 親コンポーネントから受け渡されるパラメータ
@Output() clickEvent: EventEmitter<string> = new EventEmitter(); // 親コンポーネント関数呼び出し用のemitter
constructor() { }
ngOnInit(): void {
}
clickButton() {
this.clickEvent.emit(null);
}
}
Inputのテスト
手順としては以下の通りです。 1. 変数に値を設定する(期待値) 2. 値が設定されるHTML要素を取得する(実測値) 3. 実測値と期待値が一致していることを確認する it('受け渡されたテキストが設定されること', () => {
component.text = 'Hoge input text'; // 1.変数への値設定
fixture.detectChanges(); // ※
const el = fixture.debugElement.query(By.css('span'))
.nativeElement; // 2.HTML要素の取得
expect(el.textContent).toBe(component.text); // 値の確認
});
※の fixture.detectChanges()
の目的としては、TypeScriptでのパラメータ変更を明示的にUIに反映させることです。UIに反映されていないと、2のところで取得した要素にのtextContentが設定されず、テストがNGになる可能性があります。
Outputのテスト
ここでテストするのは **clickButton()を実行した時に、Outputの関数clickEventが呼び出されること** です。ここでclickEvent()関数が呼び出されることをチェックするのに、 spyOnメソッドを用います。
spyOnの詳細は公式ドキュメントだったり他の方の記事などに譲りますが、目的としてはメソッドをモックすることです。
ある関数をテストする時に、テスト対象ではないけれど実行したいメソッドがある場合に使えます。今回の場合、
**clickButton()**メソッドのテストで
clickEventが呼び出されることを期待値としますので、以下のようにします。
it('ボタンクリック関数が親コンポーネントの関数を呼び出すこと', () => {
// clickEventのスパイを作る。
const spyObject = spyOn(component.clickEvent, 'emit');
// テスト対象メソッドの実行
component.clickButton();
// スパイのメソッドが呼ばれた(toHaveBeenCalled)ことの確認
expect(spyObject).toHaveBeenCalled();
});
イベント発火のテスト
イベント発火のわかりやすいケースとして、ボタンクリックを考えます。
<p>hoge works!</p>
<span>{{text}}</span>
<button (click)="clickButton()">Button</button>
この場合は以下のような方法でテストを行います。
- ボタン要素を取得する
- ボタンクリックイベントを発火させる
- クリック時に
clickButton()
関数が呼び出されることを確認する
関数の呼び出しをテストするので、先ほどのspyOnを使うところは同様です。
加えて、クリックイベントを発火させるということで、新しく必要になるものがあります。
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; // fakeAsync, tickのimport
...
it('ボタンクリックにより関数が実行されること', fakeAsync(() => {
const spyObject = spyOn(component, 'clickButton'); //スパイの作成
const el = fixture.debugElement.query(By.css('button')); // 1.要素の取得
el.triggerEventHandler('click', null); // 2.イベント発火
// fakeAsync 環境で非同期イベント待ちをシミュレート
tick();
expect(spyObject).toHaveBeenCalled(); //3.関数呼び出しの確認
}));
まずはテストを fakeAsync
で実行する必要があります。発火させたクリックイベントの待ちに tick()
が必要で、それにはこのテストケース実行自体を非同期で行う必要があるようです。
スパイの作成はOutputのテスト時と同様です。
要素の取得の際、ここではnativeElementとしての取得ではなく、debugElementとして取得しています。DOMとして要素のプロパティにアクセスしたいわけではないので、このようなやり方になるようです。
イベント発火にはtriggerEventHandler
を用います。
ここまでの経過
上記のテストを実行した結果が以下の通りです。 ![スクリーンショット 2020-08-20 12.16.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/562918/04ca2940-ee3c-4479-db5c-31130b2efe85.png)おわりに
サービスを通したAPIリクエストとか、画面遷移のテストなども書こうとしたのですが、たったこれだけのテストケースについてだけでも結構な分量だったので、余力があったら別途書くことにします。どういう手順で、何を用いて、何をテストすればいいのか、が掴めれば、大体同じようなコードでテストが書けるので、上記のあたりはしっかり抑えておきたいところですね。