9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-03-24

目次:Angular コンポーネントのテスト


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

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

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

$ ng generate service logger
// logger.service.ts

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

@Injectable()
export class LoggerService {

  constructor() { }

+  log(): string {
+    return 'log text';
+ }
}
// app.module.ts

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

コンポーネントの作成

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

$ ng generate component sample4
// 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();
+  }

}
// sample4.component.html

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

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

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

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

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

// 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 に関する依存の解決方法を指定したからです。

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

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

// app.module.ts

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

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

  • トークン: LoggerService
  • インスタンス: new LoggerService()

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

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

TestBed.configureTestingModule({
  ...
  providers: [
    { provide: LoggerService, useClass: LoggerService },
  ],
  • トークン(provide): LoggerService
  • インスタンスの生成方法(useClass): new LoggerService()

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

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

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

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

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

useClass

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

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 しなくて済むので便利ですね。

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

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

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 で定義します。
定義する場所によってサービスのライフサイクルが変化します。

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

// app.module.ts

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

@NgModule({
  ...
  providers: [
    FooService
  ],
// foo.component.ts

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

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

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

// 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

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

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

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

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

// foo.component.ts

@Component({
  ...
  // providers の定義がない
})
// foo.component.spec.ts

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

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

// foo.component.spec.ts

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

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

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

// foo.component.ts

@Component({
  ...
+  providers: [FooService],
})
// foo.component.spec.ts

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

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

// foo.component.spec.ts

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

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

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

// foo.component.spec.ts

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

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

次の記事:sample5 / ネストしたコンポーネントのテスト

9
9
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?