Angular コンポーネントのテスト（sample4 / サービスが注入されたコンポーネントのテスト）

目次：[Angular コンポーネントのテスト](https://qiita.com/ringtail003/items/cdd7c5427c4836857b7e) 

---

この章では（Http など Angular のビルトインサービスではなく）単純なサービスが注入されたコンポーネントのテストについて説明します。

# コンポーネントに注入するためのサービスの作成

ログ出力用のテキストを生成するサービスを作成します。

```shell
$ ng generate service logger
```

```diff
// logger.service.ts

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

@Injectable()
export class LoggerService {

  constructor() { }

+  log(): string {
+    return 'log text';
+ }
}
```

```diff
// app.module.ts

@NgModule({
  ...
  providers: [
+    LoggerService,
  ],
```

# コンポーネントの作成

テストを書くためのコンポーネントを作成します。
ボタンを押したら `<p>` にログが出力される単純なコンポーネントです。

```shell
$ ng generate component sample4
```

```diff
// sample4.component.ts

import { Component, OnInit } from '@angular/core';
+ import { LoggerService } from '../logger.service';

@Component({
  selector: 'app-sample4',
  templateUrl: './sample4.component.html',
  styleUrls: ['./sample4.component.css']
})
export class Sample4Component implements OnInit {
+  log: string;

  constructor(
+    private logger: LoggerService,
  ) { }

  ngOnInit() {
  }

+  write() {
+    this.log = this.logger.log();
+  }

}
```

```html
// sample4.component.html

- <p>
-   sample6 works!
- </p>

+ <button (click)="write()">log</button>
+ <p>{{log}}</p>
```

# アプリケーションとテスト環境のインジェクタの違い

コンポーネントにサービスへの依存を定義すると `NullInjectorError: No provider for LoggerService!` というメッセージとともにテストが落ちるはずです。

下記のコードを書き加えてみてください。今度はテストが通ります。

```diff
// sample4.component.spec.ts

import { Sample4Component } from './sample4.component';
+ import { LoggerService } from '../logger.service';


describe('Sample4Component', () => {
  let component: Sample4Component;
  let fixture: ComponentFixture<Sample4Component>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ Sample4Component ],
+        providers: [
+          { provide: LoggerService, useClass: LoggerService },
+        ],
    })
    .compileComponents();
  }));
  ...
});
```

なぜテストが通るようになったかと言うと、ソースコードから何となく分かる通り LoggerService に関する依存の解決方法を指定したからです。

## アプリケーションのインジェクタ

アプリケーション側では以下のようにサービスをインジェクタに登録しています。

```TypeScript
// app.module.ts

@NgModule({
  ...
  providers: [
    LoggerService,
  ],
```

Angular のインジェクタは依存解決のために **サービスの「トークン」と「インスタンス」** をペアで保持します。

* トークン： `LoggerService`
* インスタンス： `new LoggerService()`

## テスト環境のインジェクタ

テスト環境では、明示的に **サービスの「トークン」と「インスタンスの生成方法」** を Angular のインジェクタに登録する必要があります。

```TypeScript
TestBed.configureTestingModule({
  ...
  providers: [
    { provide: LoggerService, useClass: LoggerService },
  ],
```

* トークン（provide）： `LoggerService`
* インスタンスの生成方法（useClass）： `new LoggerService()`

依存が複数あるようなケースでは providers に全ての依存を定義します。

```TypeScript
// コンポーネントに紐付いたクラスのコンストラクタ
constructor (
  private foo: FooService,
  private bar: BarService,
  ...
) {}

// テストの依存解決
TestBed.configureTestingModule({
  providers: [
    { provide: FooService, useClass: FooService },
    { provide: BarService, useClass: BarService },
    ...
  ]
```

# テスト環境のインスタンス生成方法のパターン

テスト環境では、コンポーネントが依存するサービスの注入方法（インスタンス生成方法）を providers に渡すオブジェクトのキーとして指定します。

## useClass

`new {クラス名}()` でインスタンスを生成します。
コンポーネントが依存するサービスと同じクラス名を指定すると、そのサービスと同じ振る舞いを持ったテストダブルが生成されます。コンポーネントの振る舞いを検証するにはスパイなどを利用します。

```TypeScript
import { LoggerService } from '../logger.service';

describe('TestBed - useClass', () => {
  ...
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      ...
      providers: [
        { provide: LoggerService, useClass: LoggerService },
      ],
    })
    .compileComponents();
  }));
  ...

  it('テンプレートに出力された値を検証', () => {
    const logger = TestBed.get(LoggerService);
    spyOn(logger, 'log').and.returnValue('fake text via useClass');

    fixture.debugElement.query(By.css('button')).nativeElement.click();
    fixture.detectChanges();

    expect(fixture.debugElement.query(By.css('p')).nativeElement.textContent).toBe('fake text via useClass');
    expect(logger.log).toHaveBeenCalled();
  });
});
```

## useValue

useValueに渡した値がそのまま依存として注入されます。
まだサービスが実装されていない場合などは import しなくて済むので便利ですね。

```TypeScript
describe('TestBed - useValue', () => {
  ...
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      ...
      providers: [
        { provide: LoggerService, useValue: { log: () => 'fake text via useValue' } },
      ],
    })
    .compileComponents();
  }));
  ...

  it('テンプレートに出力された値を検証', () => {
    fixture.debugElement.query(By.css('button')).nativeElement.click();
    fixture.detectChanges();

    expect(fixture.debugElement.query(By.css('p')).nativeElement.textContent).toBe('fake text via useValue');
  });
});

```

## useFactory

依存解決のためのファクトリ関数を指定します。
依存サービスの初期設定などが必要な場合に便利ですね。

```TypeScript
describe('TestBed - useFactory', () => {
  ...
  class FakeLogger {
    text: string;
    log(): string {
      return this.text;
    }
  }

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      ...
      providers: [
        { provide: LoggerService, useFactory: () => {
          const logger = new FakeLogger();
          logger.text = 'fake text via useFactory';

          return logger;
        } },
      ],
    })
    .compileComponents();
  }));

  ...

  it('テンプレートに出力された値を検証', () => {
    fixture.debugElement.query(By.css('button')).nativeElement.click();
    fixture.detectChanges();

    expect(fixture.debugElement.query(By.css('p')).nativeElement.textContent).toBe('fake text via useFactory');
  });
});
```

# インジェクタとサービスのライフサイクル

Angular ではコンポーネントごとに専用のインジェクタが存在します。
コンポーネントがツリー構造になっているのと同様に、インジェクタもツリー構造になっています。

依存サービスを解決する時 Angular はまず最初に、コンポーネントのインジェクタにトークンを問い合わせます。
トークンが見つからない場合は、親コンポーネントのインジェクタに問い合わせます。
そこでもトークンが見つからない場合は、さらにその親コンポーネントに問い合わせ、最上位のルートインジェクタに到達するまで問い合わせを繰り返します。

一番最初に目にした `NullInjectorError: No provider for LoggerService!` は「（テスト環境の）ルートインジェクタまで遡ってトークンを検索したが見つからなかった」という例外処理のメッセージだった訳ですね。

インジェクタへのサービス登録は NgModule または Component デコレータの `providers` で定義します。
定義する場所によってサービスのライフサイクルが変化します。

## ルートインジェクタにサービスを登録

```TypeScript
// app.module.ts

import { FooService } from './foo.service.ts';

@NgModule({
  ...
  providers: [
    FooService
  ],
```

```TypeScript
// foo.component.ts

@Component({
  ...
})
export class FooComponent implements OnInit {
  constructor(
    private foo: FooService,
  ) { }
```

依存サービスのインスタンスは **アプリケーション実行時に生成され、ルートインジェクタに登録、アプリケーション終了時に破棄** されます。

## コンポーネントのインジェクタにサービスを登録

```TypeScript
// foo.component.ts

import { FooService } from '../foo.service.ts';

@Component({
  ...
  providers: [FooService],
})
export class FooComponent implements OnInit {
  constructor(
    private foo: FooService,
  ) { }
```

依存サービスのインスタンスは **コンポーネントと同時に生成され、コンポーネントのインジェクタに登録、 コンポーネントが解放される時に破棄** されます。

> 公式ドキュメント **Hierarchical Dependency Injectors**
> https://angular.io/guide/hierarchical-dependency-injection


# ライフサイクルに合わせたテスト環境におけるサービス登録

「依存サービスがどのインジェクタに登録されているか」によって、テスト環境で使用するインジェクタが変わります。

## ルートインジェクタまたは親コンポーネントのインジェクタに登録されているサービス

テスト環境のモジュールのインジェクタに依存サービスを登録します。

```TypeScript
// foo.component.ts

@Component({
  ...
  // providers の定義がない
})
```

```diff
// foo.component.spec.ts

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [ FooComponent ],
+    providers: [
+      { provide: FooService, useClass: FooService },
+    ],
  })
  .compileComponents();
}));
```

依存サービスをスパイに置き換えるような場合、テスト環境のモジュールのインジェクタにアクセスします。

```TypeScript
// foo.component.spec.ts

const foo = TestBed.get(FooService);
spyOn(foo, 'log');
```

## コンポーネントのインジェクタに登録されているサービス

TestBed.overrideComponent を利用してコンポーネントのインジェクタに依存サービスを登録します。

```TypeScript
// foo.component.ts

@Component({
  ...
+  providers: [FooService],
})
```

```diff
// foo.component.spec.ts

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [ FooComponent ],
  })
+  .overrideComponent(FooComponent, {
+    set: {
+      providers: [
+        { provide: FooService, useClass: FooService },
+      ],
+    }
+  })
  .compileComponents();
}));
```

依存サービスをスパイに置き換えるような場合、コンポーネントのインジェクタにアクセスします。

```TypeScript
// foo.component.spec.ts

const foo = fixture.debugElement.injector.get(FooService);
spyOn(foo, 'log');
```

## コンポーネントのインジェクタに登録されているサービスは、サービス登録しなくてもテスト可能

コンポーネントのインジェクタに依存サービスが登録されている場合は、テスト環境でわざわざサービス登録をしなくてもコンパイルは通ります。
「テスト環境でサービスを登録する事」は「サービスをテストダブルに置き換える事」と等価であり、テストダブルを使わないのであれば何もしなくて良いでしょう。

```TypeScript
// foo.component.spec.ts

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [ FooComponent ],
    // providers: [...]
  })
  // .overrideComponent(...)
  .compileComponents();
}));

it('should create', () => {
  expect(component).toBeTruthy();
});
```

---

次の記事：[sample5 / ネストしたコンポーネントのテスト](https://qiita.com/ringtail003/items/baf47ce2a768bdadb0d8)