Marginalia

Angular Testing: TestBedにはdeclarationsではなくimportsを設定する


コンポーネントのテストにおいて、TestBed.configureTestingModule()declarations を設定するユースケースはそれほど多くない。

Angular CLI の ng generate component コマンドで生成される spec ファイルが次のようなコードをスキャフォールドするため、それをそのまま使わなければならないと勘違いしている開発者も多いが、スキャフォールドはお手本ではない

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooComponent } from './foo.component';

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

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

    beforeEach(() => {
        fixture = TestBed.createComponent(FooComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

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

TestBed に declarations を設定しない

  • TestBed に declarations を設定してコンポーネントテストをすると面倒なことがいくつかある
    • 対象コンポーネントの子コンポーネントが解決できない
    • コンポーネントのコンストラクタで注入される依存オブジェクトが提供されていない
      • importsproviders でセットアップする
      • spec ファイル側での imports 忘れ
        • アプリケーションコードで新しく実装するたびにするたびにテスト側でも同じモジュールを追加する
        • アプリケーションコードでは不要になったモジュールをテスト側に残り続けることもしばしば
  • TestBed.configureTestingModule()の目的
    1. テスト対象の依存関係解決
    2. テストダブルのセットアップ
    • テストダブルのセットアップはテストだけの関心なのでそのままで問題ない
  • 同じコンポーネントを二度宣言しない
    • アプリケーション側でそのコンポーネントを declarations に追加しているモジュールがすでにあるはず
    • TestBed でその NgModule をインポートすればテスト対象の依存関係解決は達成されるはず
  • コンポーネントのテストが同時にその NgModule のテストにもなる
    • 解決されるべき依存関係が解決されないときテストが失敗する
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooComponent } from './foo.component';
import { FooModule } from './foo.module';

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

    beforeEach(async () => {
        await TestBed.configureTestingModule({
            imports: [FooModule], // 対象のコンポーネントを提供するモジュールをインポートするだけ
        }).compileComponents();
    });

    beforeEach(() => {
        fixture = TestBed.createComponent(FooComponent); // FooModuleで宣言されているため生成できる
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

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

NgModule を分割するモチベーション

  • すべてをコンポーネントが AppModule で宣言されていると上記のアプローチはとりづらい
    • 対象コンポーネントと関係ない依存オブジェクトが初期化されるオーバーヘッドが無駄
    • AppModuleにはアプリケーションの初期化に閉じた関心(いわゆる forRoot())が多くあり、ユニットテストで読み込まれるのが不都合な場面もある
  • 再利用可能な NgModule を分割しておくことはAppModuleの肥大化を防ぐだけでなくユニットテストの書きやすさにもつながる

TestBed に declarations を設定するユースケース

  • TestHost を使うテストケース
    • Angular 日本語ドキュメンテーション - コンポーネントのテストシナリオ
      • 対象コンポーネントを直接テストするのではなくテンプレート経由でテスト用のホストコンポーネントを用意する
    • この場合 declarations にはテストホストだけがあり、その依存関係を解決するために対象コンポーネントの NgModule を imports に追加すればよい
      • テストホストを使っても使わなくても imports: [FooModule] は変わらず有用である
    • ディレクティブのテストも基本的にこの形になる
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FooDirective } from './foo.directive';
import { FooModule } from './foo.module';

@Component({
    template: `<div appFoo></div>`
})
class TestHostComponent {}

describe('FooDirective', () => {
    let host: TestHostComponent;
    let fixture: ComponentFixture<TestHostComponent>;

    beforeEach(async () => {
        await TestBed.configureTestingModule({
            declarations: [TestHostComponent], // テストホストの宣言
            imports: [FooModule], // 対象のディレクティブを提供するモジュール
        }).compileComponents();
    });

    beforeEach(() => {
        fixture = TestBed.createComponent(TestHostComponent); // FooModuleで宣言されているため生成できる
        host = fixture.componentInstance;
        fixture.detectChanges();
    });

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