コンポーネントのテストにおいて、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
を設定してコンポーネントテストをすると面倒なことがいくつかある- 対象コンポーネントの子コンポーネントが解決できない
-
schemas: [NO_ERRORS_SCHEMA]
で解消されがち- テンプレートがコンパイルエラーになっていることに気づかずデプロイ前のビルドで発覚することもしばしば
- NO_ERRORS_SCHEMA を安易に使うのをやめたい話 - とんかつ時々あんどーなつ
-
- コンポーネントのコンストラクタで注入される依存オブジェクトが提供されていない
-
imports
やproviders
でセットアップする - spec ファイル側での
imports
忘れ- アプリケーションコードで新しく実装するたびにするたびにテスト側でも同じモジュールを追加する
- アプリケーションコードでは不要になったモジュールをテスト側に残り続けることもしばしば
-
- 対象コンポーネントの子コンポーネントが解決できない
-
TestBed.configureTestingModule()
の目的- テスト対象の依存関係解決
- テストダブルのセットアップ
- テストダブルのセットアップはテストだけの関心なのでそのままで問題ない
- 同じコンポーネントを二度宣言しない
- アプリケーション側でそのコンポーネントを
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]
は変わらず有用である
- テストホストを使っても使わなくても
- ディレクティブのテストも基本的にこの形になる
- Angular 日本語ドキュメンテーション - コンポーネントのテストシナリオ
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();
});
});