Help us understand the problem. What is going on with this article?

Angularの単体テストの書き方試行錯誤したのを残しておく

はじめに

Angularの単体テストって

「デフォルトでフレームワークとテストランナー(Jasmine, karma)が入っているからやりやすい!」
「テストの実行が簡単」

と言われていることが多い気がするのですが、テスト自体を書くのはかなり難易度が高いように感じました。
単純に自前で作った関数の実行だけだったらともかく、アプリケーション全体としてテストを書いていこうとしたら、初見では未知なことが非常に多い印象でした。

さらに困ったことに、各種テスト(コンポーネントのテスト、UI操作イベントのテスト、APIリクエスト)の書き方がまとまった形で書かれているドキュメントなどにも出会えなかったため、毎度毎度テストケースを追加するたびににググってstackoverflowにたどり着いてどうにか書いて、と繰り返す羽目になっていました。

このへんがだいぶ面倒だったので、フロントエンドのアプリケーションでよくありそうな実装に対して、単体テストの書き方をここにまとめてみます。

前提

フロントエンドのアプリケーションは以下のようなものです。
- Angular 9
- Jasmine/Karma (Angular CLIでのインストール時のデフォルト)
- UIフレームワーク: angular material

各種テストケース

目次
- コンポーネントが作成されることのテスト
- コンポーネント内の要素のテスト
- コンポーネント間のデータ受け渡しのテスト
- イベント発火のテスト

初期状態

コンポーネントをコマンドラインで作成した時に自動的に作られるテストファイル(spec.ts)は以下のようなものです

hoge.component.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要素は

を持つのみです。

hoge.component.html
<p>hoge works!</p>

このようにstaticなデータを持つ場合には、 p要素が"hoge works!"であることというのがテストとして考えられます。
その場合には以下のようなテストケースを追加することになります

hoge.component.spec.ts
...
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が使われます。最小のコードは以下のようなものです。

hoge.component.html
<p>hoge works!</p>
<span>{{text}}</span>
<button (click)="clickButton()">Button</button>
hoge.component.ts
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. 実測値と期待値が一致していることを確認する

hoge.component.spec.ts
    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が呼び出されることを期待値としますので、以下のようにします。

hoge.component.spec.ts
    it('ボタンクリック関数が親コンポーネントの関数を呼び出すこと', () => {
      // clickEventのスパイを作る。
      const spyObject = spyOn(component.clickEvent, 'emit');

      // テスト対象メソッドの実行
      component.clickButton();

     // スパイのメソッドが呼ばれた(toHaveBeenCalled)ことの確認
      expect(spyObject).toHaveBeenCalled();
    });

イベント発火のテスト

イベント発火のわかりやすいケースとして、ボタンクリックを考えます。

hoge.component.html
<p>hoge works!</p>
<span>{{text}}</span>
<button (click)="clickButton()">Button</button>

この場合は以下のような方法でテストを行います。
1. ボタン要素を取得する
2. ボタンクリックイベントを発火させる
3. クリック時にclickButton()関数が呼び出されることを確認する

関数の呼び出しをテストするので、先ほどのspyOnを使うところは同様です。
加えて、クリックイベントを発火させるということで、新しく必要になるものがあります。

hoge.component.spec.ts
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

おわりに

サービスを通したAPIリクエストとか、画面遷移のテストなども書こうとしたのですが、たったこれだけのテストケースについてだけでも結構な分量だったので、余力があったら別途書くことにします。

どういう手順で、何を用いて、何をテストすればいいのか、が掴めれば、大体同じようなコードでテストが書けるので、上記のあたりはしっかり抑えておきたいところですね。

topgate
Google技術を中心に取り扱う技術者集団
https://www.topgate.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした