Angular でコンポーネントとサービスのテストをする

# はじめに

* Angular でコンポーネントとサービスを普通にテストする方法をまとめる。
* 取り扱うケースは以下
  * コンポーネントの初期化のテスト
  * コンポーネントのクリックイベントのテスト
  * 親子関係のあるコンポーネントの初期化のテスト
  * コンポーネントからサービスを初期化するテスト

# コンポーネントの初期化のテスト

* コンポーネントクラスを定義し、テストモジュール経由で初期化するテスト。

```ts

import {Component} from '@angular/core';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';

// コンポーネントの定義
@Component({
  selector: 'parent-component',
  template: '<p>parent component working.</p>'
})
class ParentComponent {
  name = 'parent component';
}

// テスト
describe('コンポーネントを初期化するだけのテスト', () => {
  // コンポーネントの雛形
  let fixture: ComponentFixture<ParentComponent>;
  // コンポーネント
  let comp: ParentComponent;

  // 事前にコンポーネントを作成する
  beforeEach(async(() => {
    // テスト環境の @NgModule の設定
    TestBed.configureTestingModule({
      declarations: [ParentComponent]
    });
    // コンポーネントを作成して変数にセット
    TestBed.compileComponents().then(() => {
      fixture = TestBed.createComponent(ParentComponent);
      comp = fixture.componentInstance;
    });
  }));

  // テストの実行
  it('プロパティの初期値が正常に取得できる', () => {
    // プロパティのチェック
    expect(comp.name).toBe('parent component');
  });
});
```


# コンポーネントのクリックイベントのテスト

* クリックイベントをハンドリングするボタンを持ったコンポーネントを定義して、テストモジュール経由で初期化しクリックイベントを発火させるテスト。
* ほぼ前出のものと一緒なので変更点だけ。全文は[こちら](https://gist.github.com/nirasan/ef1f6e7459a9c95dbc16cad3008a4566#file-sample2-spec-ts)。

## クリックイベントをハンドリングするボタンの定義

* ボタン押下で onClick が実行されるように。

```ts
// コンポーネントの定義
@Component({
  selector: 'parent-component',
  template: '<button (click)="onClick()">btn</button>'
})
class ParentComponent {
  name = 'parent component';
  onClick() {
    this.name = 'clicked';
  }
}
```

## クリックイベントの発火のテスト

```ts
  // テストの実行
  it('クリックイベントを実行してメソッドが実行されている', fakeAsync(() => {
    // onClick メソッドを監視
    spyOn(comp, 'onClick');
    // ボタンを取得してクリックイベントを送信
    const btn = fixture.debugElement.query(By.css('button'));
    btn.triggerEventHandler('click', null);
    // fakeAsync 環境で非同期イベント待ちをシミュレート
    tick();
    // 更新を検知
    fixture.detectChanges();
    // onClick メソッドが実行されたことの確認
    expect(comp.onClick).toHaveBeenCalled();
  }));
```


# 親子関係のあるコンポーネントの初期化のテスト

* 親子関係のあるコンポーネントを定義して、テストモジュール経由での初期化後に子コンポーネントを取得してテストする。
* こちらもほぼ前出のものと同じなので要点のみで全文は[こちら](https://gist.github.com/nirasan/ef1f6e7459a9c95dbc16cad3008a4566#file-sample3-spec-ts)。

## 親子関係のあるコンポーネント定義

```ts
// コンポーネントの定義
@Component({
  selector: 'parent-component',
  template: '<p>parent component working.</p> <child-component></child-component>'
})
class ParentComponent {
  name = 'parent component';
}

// 子コンポーネントの定義
@Component({
  selector: 'child-component',
  template: '<p>child component working</p>'
})
class ChildComponent {
  name = 'child component';
}
```

## 子コンポーネントの取得とテスト

```ts
  // テストの実行
  it('子コンポーネントを取得してプロパティが取得できることをテストする', () => {
    // CSS で子コンポーネントの HTML 要素取得
    const el = fixture.debugElement.query(By.css('child-component'));
    // HTML 要素からコンポーネントを取得
    const child = el.componentInstance;
    // プロパティのチェック
    expect(comp.name).toBe('parent component');
    expect(child.name).toBe('child component');
  });
```


# コンポーネントからサービスを初期化するテスト

* サービスだけならただ new してテストすればいいがコンポーネントとの連携をテストしたいなどの場合。
* コンポーネントとサービスを定義し、テストモジュール経由で初期化するテスト。
* これも要点のみで。全文は[こちら](https://gist.github.com/nirasan/ef1f6e7459a9c95dbc16cad3008a4566#file-sample4-spec-ts)。

## サービスとコンポーネントの定義

```ts
// サービスの定義
@Injectable()
class TestService {
  name = 'test service';
}

// 子コンポーネントの定義
@Component({
  selector: 'child-component',
  template: '<p>child component working</p>'
})
class ChildComponent {
  name = 'child component';
  constructor(public testService: TestService) { } // DI でサービスを初期化する
}
```

## サービスの DI 設定

* テストモジュールの設定でサービスが DI されるようにする。

```ts
    TestBed.configureTestingModule({
      declarations: [ParentComponent, ChildComponent],
      providers: [TestService], // TestService が DI されるように設定
    });
```

## サービスのテスト

* サービスがコンポーネント経由で初期化されているかどうかをテスト

```ts
    expect(child.testService.name).toBe('test service');
```

## `Failed: Can't resolve all parameters for ChildComponent`

* `TestService` を `ChildComponent` よりも後に定義すると `Failed: Can't resolve all parameters for ChildComponent` というエラーになり正常に実行できなかった。
* コンパイルエラーにはならないが、依存関係のあるクラスの定義と実行を同一ファイルで行う場合は、被依存クラスを依存クラスよりも前の行で定義しないと実行時エラーになる様子。

# 参考

* [Angular Testing](https://angular.io/guide/testing)