/var/log/messages

May 8, 2018 - 97 minute read - Comments - angular

Angular Testing Guide

以下に目を通していたのですがぐぐる翻訳にかけてみたので控えを以下に。

断りは入れておりません。ドラフトです。

このガイドでは、単位アプリケーションと統合テストのヒントとテクニックについて説明します。

このガイドでは、Tour of Heroesチュートリアルによく似たサンプルCLIアプリケーションのテストを紹介します。 サンプルアプリケーションとこのガイドのすべてのテストは、検査と実験に使用できます。

Setup

Angular CLIは、Jasmine test framework でAngularアプリケーションをテストするために必要なものをすべてダウンロードしてインストールします。

CLIを使用して作成したプロジェクトは、すぐにテストする準備ができています。 この1つのCLIコマンドを実行するだけです。

ng test

ng testコマンドは watch mode でアプリをビルドし、karma test runner を起動します。

コンソール出力は次のように少し見えます:

10% building modules 1/1 modules 0 active
...INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
...INFO [launcher]: Launching browser Chrome ...
...INFO [launcher]: Starting browser Chrome
...INFO [Chrome ...]: Connected on socket ... 
Chrome ...: Executed 3 of 3 SUCCESS (0.135 secs / 0.205 secs)

ログの最後の行が最も重要です。 それは Karma がすべて3つのテストを実行したことを示しています。

クロムブラウザも開き、このように “Jasmine HTML Reporter"にテスト出力を表示します。

Jasmine HTML Reporter in the browser

ほとんどの人は、このブラウザの出力がコンソールログより読みやすくなっています。 テスト行をクリックしてそのテストだけを再実行するか、説明をクリックして、選択したテストグループ(「テストスイート」)でテストを再実行することができます。

一方、ng testコマンドは変更を監視しています。

この動作を確認するには、app.component.tsを少し変更して保存します。 テストが再度実行され、ブラウザがリフレッシュされ、新しいテスト結果が表示されます。

Configuration

CLIはJasmineとKarma の構成を担当します。

src/フォルダー内のkarma.conf.jsファイルとtest.tsファイルを編集することで、多くのオプションを微調整できます。

karma.conf.jsファイルはカルマ構成ファイルの一部です。 CLIは、angular.jsonファイルで指定されたアプリケーション構造に基づいて、karma.conf.jsによって補完されたメモリ内の完全な実行時設定を構築します。

JasmineとKarma の構成の詳細については、Webで検索してください。

Other Test Framework

他のテストライブラリやテストランナーとAngularアプリをユニットテストすることもできます。 各ライブラリとランナーには、独自のインストール手順、構成、および構文があります。

詳細はウェブで検索してください。

Test file name and location

src/appフォルダーを見てください。

CLIはapp.component.spec.tsという名前のAppComponentのテストファイルを生成しました。

テストファイル拡張子は.spec.tsでなければなりません。これにより、ツールでファイルをテストファイル(AKA、スペックファイル)として識別できるようになります。

app.component.tsファイルとapp.component.spec.tsファイルは、同じフォルダ内の兄弟です。 ルートファイル名(app.component)は、両方のファイルで同じです。

すべての種類のテストファイルに対して、この2つの規則を自分のプロジェクトで採用してください。

Service Test

サービスはしばしばユニットテストのための最も簡単なファイルです。 ここでは、Angular testingユーティリティの助けなしに書かれたValueServiceの同期テストと非同期テストのいくつかを示します。

app/demo/demo.spec.ts

    1. // Straight Jasmine testing without Angular's testing support
    2. describe('ValueService', () => {
    3.   let service: ValueService;
    4.   beforeEach(() => { service = new ValueService(); });
    5.  
    6.   it('#getValue should return real value', () => {
    7.     expect(service.getValue()).toBe('real value');
    8.   });
    9.  
    10.   it('#getObservableValue should return value from observable',
    11.     (done: DoneFn) => {
    12.     service.getObservableValue().subscribe(value => {
    13.       expect(value).toBe('observable value');
    14.       done();
    15.     });
    16.   });
    17.  
    18.   it('#getPromiseValue should return value from a promise',
    19.     (done: DoneFn) => {
    20.     service.getPromiseValue().then(value => {
    21.       expect(value).toBe('promise value');
    22.       done();
    23.     });
    24.   });
    25. });

Services with dependencies

サービスは、Angularがコンストラクタに注入する他のサービスに依存することがよくあります。 多くの場合、サービスのコンストラクタを呼び出している間に、これらの依存関係を手作業で作成して注入するのは簡単です。

MasterServiceは簡単な例です:

app/demo/demo.ts

@Injectable()
export class MasterService {
  constructor(private valueService: ValueService) { }
  getValue() { return this.valueService.getValue(); }
}

MasterServiceは、唯一のメソッドgetValueを注入されたValueServiceに委譲します。

テストする方法はいくつかあります。

app/demo/demo.spec.ts

    1. describe('MasterService without Angular testing support', () => {
    2.   let masterService: MasterService;
    3.  
    4.   it('#getValue should return real value from the real service', () => {
    5.     masterService = new MasterService(new ValueService());
    6.     expect(masterService.getValue()).toBe('real value');
    7.   });
    8.  
    9.   it('#getValue should return faked value from a fakeService', () => {
    10.     masterService = new MasterService(new FakeValueService());
    11.     expect(masterService.getValue()).toBe('faked service value');
    12.   });
    13.  
    14.   it('#getValue should return faked value from a fake object', () => {
    15.     const fake =  { getValue: () => 'fake value' };
    16.     masterService = new MasterService(fake as ValueService);
    17.     expect(masterService.getValue()).toBe('fake value');
    18.   });
    19.  
    20.   it('#getValue should return stubbed value from a spy', () => {
    21.     // create `getValue` spy on an object representing the ValueService
    22.     const valueServiceSpy =
    23.       jasmine.createSpyObj('ValueService', ['getValue']);
    24.  
    25.     // set the value to return when the `getValue` spy is called.
    26.     const stubValue = 'stub value';
    27.     valueServiceSpy.getValue.and.returnValue(stubValue);
    28.  
    29.     masterService = new MasterService(valueServiceSpy);
    30.  
    31.     expect(masterService.getValue())
    32.       .toBe(stubValue, 'service returned stub value');
    33.     expect(valueServiceSpy.getValue.calls.count())
    34.       .toBe(1, 'spy method was called once');
    35.     expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
    36.       .toBe(stubValue);
    37.   });
    38. });

最初のテストでは、newでValueServiceを作成し、それをMasterServiceコンストラクタに渡します。

しかし、実際のサービスを注入することは、ほとんどの依存サービスが作成および制御するのが難しいため、ほとんど機能しません。

その代わりに、依存関係を模擬したり、ダミー値を使用したり、適切なサービス方法でスパイを作成することができます。

彼らは通常、サービスを模擬する最も簡単な方法なので、スパイを好む。

これらの標準的なテスト手法は、単体テストサービスに最適です。

しかし、Angular dependency injectionを使用してアプリケーションクラスにサービスを注入する場合は、ほとんどの場合、その使用パターンを反映したテストが必要です。 Angular testing utility を使用すると、注入されたサービスの動作を簡単に調べることができます。

Testing services with the TestBed

あなたのアプリは Angular の依存性注入(DI)を利用してサービスを作成しています。 サービスに従属サービスがある場合、DIはその従属サービスを検出または作成します。 その依存サービスが独自の依存関係を持っている場合、DIはそれらを検出または作成します。

サービス消費者として、あなたはこれについて心配する必要はありません。 あなたはコンストラクタの引数の順序や生成方法について心配する必要はありません。

サービス・テスターとしては、少なくともサービス依存の第1レベルについて考える必要がありますが、TestBedテスト・ユーティリティーを使用してサービスを提供し、作成するときに、Angular DIにサービス作成を依頼し、コンストラクター・アーギュメントの順序を処理させることができます。

Angular TestBed

TestBedはAngular testingユーティリティの中で最も重要です。 TestBedは、Angular @NgModuleをエミュレートする動的に構築されたAngularテストモジュールを作成します。

TestBed.configureTestingModule() メソッドは、@NgModuleのほとんどのプロパティを持つことができるメタデータオブジェクトを取ります。

サービスをテストするには、プロバイダのメタデータプロパティを、テストまたはモックする一連のサービスで設定します。

app/demo/demo.testbed.spec.ts (provide ValueService in beforeEach

let service: ValueService;

beforeEach(() => {
  TestBed.configureTestingModule({ providers: [ValueService] });
});

次に、サービスクラスを引数としてTestBed.get()を呼び出すことで、テスト内に注入します。

it('should use ValueService', () => {
  service = TestBed.get(ValueService);
  expect(service.getValue()).toBe('real value');
});

または、あなたの設定の一部としてサービスを注入したい場合は、beforeEach() の中に入れます。

beforeEach(() => {
  TestBed.configureTestingModule({ providers: [ValueService] });
  service = TestBed.get(ValueService);
});

依存関係を持つサービスをテストするときは、プロバイダ配列にモックを提供してください。

次の例では、モックはスパイオブジェクトです。

let masterService: MasterService;
let valueServiceSpy: jasmine.SpyObj<ValueService>;

beforeEach(() => {
  const spy = jasmine.createSpyObj('ValueService', ['getValue']);

  TestBed.configureTestingModule({
    // Provide both the service-to-test and its (spy) dependency
    providers: [
      MasterService,
      { provide: ValueService, useValue: spy }
    ]
  });
  // Inject both the service-to-test and its (spy) dependency
  masterService = TestBed.get(MasterService);
  valueServiceSpy = TestBed.get(ValueService);
});

このテストは以前と同じようにスパイを消費します。

it('#getValue should return stubbed value from a spy', () => {
  const stubValue = 'stub value';
  valueServiceSpy.getValue.and.returnValue(stubValue);

  expect(masterService.getValue())
    .toBe(stubValue, 'service returned stub value');
  expect(valueServiceSpy.getValue.calls.count())
    .toBe(1, 'spy method was called once');
  expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
    .toBe(stubValue);
});

Teting without BeforeEach()

このガイドのほとんどのテストスイートはbeforeEach()を呼び出してit()テストの前提条件を設定し、TestBedを使用してクラスを作成し、サービスを注入します。

beforeEach() とtestBedを使用するのではなく、明示的にクラスを作成することを決して望んでいないテストの別の学校があります。

MasterServiceテストの1つをそのスタイルで書き直す方法は次のとおりです。

beforeEach()の代わりに、再利用可能な準備コードをセットアップ関数に入れて始めます。

app/demo/demo.spec.ts (setup)

function setup() {
  const valueServiceSpy =
    jasmine.createSpyObj('ValueService', ['getValue']);
  const stubValue = 'stub value';
  const masterService = new MasterService(valueServiceSpy);

  valueServiceSpy.getValue.and.returnValue(stubValue);
  return { masterService, stubValue, valueServiceSpy };
}

setup()関数は、テストによって参照される変数(masterServiceなど)を含むオブジェクトリテラルを返します。 describe()の本体に準グローバル変数(たとえば、masterService:MasterService)を定義しません。

次に、各テストでは、最初の行でsetup()を呼び出してから、テスト対象を操作して期待値をアサートします。

it('#getValue should return stubbed value from a spy', () => {
  const { masterService, stubValue, valueServiceSpy } = setup();
  expect(masterService.getValue())
    .toBe(stubValue, 'service returned stub value');
  expect(valueServiceSpy.getValue.calls.count())
    .toBe(1, 'spy method was called once');
  expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
    .toBe(stubValue);
});

テストでは、必要なセットアップ変数を抽出するために、非構造化割り当てを使用する方法に注目してください。

const { masterService, stubValue, valueServiceSpy } = setup();

多くの開発者は、このアプローチが従来のbeforeEach()スタイルよりも明快で明確であると感じています。

このテストガイドは伝統的なスタイルに従いますが、デフォルトのCLI回路はbeforeEach() とTestBedでテストファイルを生成しますが、自分のプロジェクトでこの代替アプローチを採用することは自由です。

Testing HTTP Service

リモートサーバーへのHTTP呼び出しを行うデータサービスは、通常、XHR呼び出しのためにAngular HttpClientサービスに注入し、委任します。

依存関係を持つサービスをテストする場合と同様に、注入されたHttpClientスパイでデータサービスをテストすることができます。

    1. let httpClientSpy: { get: jasmine.Spy };
    2. let heroService: HeroService;
    3.  
    4. beforeEach(() => {
    5.   // TODO: spy on other methods too
    6.   httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
    7.   heroService = new HeroService(<any> httpClientSpy);
    8. });
    9.  
    10. it('should return expected heroes (HttpClient called once)', () => {
    11.   const expectedHeroes: Hero[] =
    12.     [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
    13.  
    14.   httpClientSpy.get.and.returnValue(asyncData(expectedHeroes));
    15.  
    16.   heroService.getHeroes().subscribe(
    17.     heroes => expect(heroes).toEqual(expectedHeroes, 'expected heroes'),
    18.     fail
    19.   );
    20.   expect(httpClientSpy.get.calls.count()).toBe(1, 'one call');
    21. });
    22.  
    23. it('should return an error when the server returns a 404', () => {
    24.   const errorResponse = new HttpErrorResponse({
    25.     error: 'test 404 error',
    26.     status: 404, statusText: 'Not Found'
    27.   });
    28.  
    29.   httpClientSpy.get.and.returnValue(asyncError(errorResponse));
    30.  
    31.   heroService.getHeroes().subscribe(
    32.     heroes => fail('expected an error, not heroes'),
    33.     error  => expect(error.message).toContain('test 404 error')
    34.   );
    35. });

HeroServiceメソッドはObservablesを返します。 オブザーバブルに加入して、(a)実行させ、(b)メソッドが成功するか失敗するかを宣言する必要があります。

subscribe() メソッドは、成功(次)および失敗(エラー)コールバックをとります。 両方のコールバックを提供して、エラーをキャプチャするようにしてください。 これを怠ると、テストランナーが完全に異なるテストに属している可能性が高い、非同期で観察不能なエラーが生成されます。

HttpClientTestingModule

データサービスとHttpClientとの間の拡張されたやりとりは、スパイと偽装するのが複雑で難しい場合があります。

HttpClientTestingModuleは、これらのテストシナリオをより管理しやすくすることができます。

このガイドに付随するコードサンプルはHttpClientTestingModuleを示していますが、このページはHttpClientTestingModuleを使ったテストについて詳しく説明しているHttp guideを参照してください。

このガイドのサンプルコードでは、app/model/http-hero.service.spec.ts の従来のHttpModuleのテストも示しています。

Component Test Basics

コンポーネントは、Angularアプリケーションの他のすべての部分と異なり、HTMLテンプレートとTypeScriptクラスを組み合わせています。 コンポーネントは本当にテンプレートと一緒に働くクラスです。 コンポーネントを適切にテストするには、それらが意図したとおりに動作することをテストする必要があります。

このようなテストでは、AngularのようにブラウザのDOMにコンポーネントのホスト要素を作成し、そのテンプレートに記述されているようにコンポーネントクラスのDOMとのやりとりを調べる必要があります。

Angular TestBedは、以下のセクションで説明するように、この種のテストを容易にします。 しかし、多くの場合、DOM関与なしでコンポーネントクラスだけをテストすると、コンポーネントの動作の多くをより簡単でより明白な方法で検証できます。

Component class testing

サービスクラスをテストするのと同じように、コンポーネントクラスを独自にテストします。

ユーザーがボタンをクリックしたときにライトをオン/オフ(オンスクリーンメッセージで表される)に切り替えるこのLightswitchComponentを考えてみましょう。

app/demo/demo.ts (LightswitchComp)

@Component({
  selector: 'lightswitch-comp',
  template: `
    <button (click)="clicked()">Click me!</button>
    <span>{{message}}</span>`
})
export class LightswitchComponent {
  isOn = false;
  clicked() { this.isOn = !this.isOn; }
  get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}

clicked()メソッドがライトのオン/オフ状態を切り替えるだけで、メッセージを適切に設定することをテストすることができます。

このコンポーネントクラスは依存関係がありません。 依存関係のないサービスをテストするには、新しいサービスを作成し、そのAPIを突き刺して、パブリック状態の期待を表明します。 コンポーネントクラスと同じ操作を行います。

app/demo/demo.spec.ts (Lightswitch tests)

describe('LightswitchComp', () => {
  it('#clicked() should toggle #isOn', () => {
    const comp = new LightswitchComponent();
    expect(comp.isOn).toBe(false, 'off at first');
    comp.clicked();
    expect(comp.isOn).toBe(true, 'on after click');
    comp.clicked();
    expect(comp.isOn).toBe(false, 'off after second click');
  });

  it('#clicked() should set #message to "is on"', () => {
    const comp = new LightswitchComponent();
    expect(comp.message).toMatch(/is off/i, 'off at first');
    comp.clicked();
    expect(comp.message).toMatch(/is on/i, 'on after clicked');
  });
});

ここでは、Tour of HeroesチュートリアルのDashboardHeroComponentを紹介します。

app/dashboard/dashboard-hero.component.ts (component)

export class DashboardHeroComponent {
  @Input() hero: Hero;
  @Output() selected = new EventEmitter<Hero>();
  click() { this.selected.emit(this.hero); }
}

hero を@Inputプロパティにバインドし、選択された@Outputプロパティを使用して生成されたイベントをリッスンする親コンポーネントのテンプレート内に表示されます。

DashboardHeroComponentまたはその親コンポーネントを作成せずにクラスコードが動作することをテストできます。

app/dashboard/dashboard-hero.component.spec.ts (class tests)

it('raises the selected event when clicked', () => {
  const comp = new DashboardHeroComponent();
  const hero: Hero = { id: 42, name: 'Test' };
  comp.hero = hero;

  comp.selected.subscribe(selectedHero => expect(selectedHero).toBe(hero));
  comp.click();
});

コンポーネントに依存関係がある場合、TestBedを使用してコンポーネントとその依存関係を作成することができます。

次のWelcomeComponentは、UserServiceに依存して、挨拶するユーザーの名前を知っています。

app/welcome/welcome.component.ts

export class WelcomeComponent  implements OnInit {
  welcome: string;
  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.welcome = this.userService.isLoggedIn ?
      'Welcome, ' + this.userService.user.name : 'Please log in.';
  }
}

まず、このコンポーネントの最小限のニーズを満たすUserServiceのモックを作成します。

app/welcome/welcome.component.spec.ts (MockUserService)

class MockUserService {
  isLoggedIn = true;
  user = { name: 'Test User'};
};

次に、コンポーネントとサービスの両方をTestBed構成で提供して注入します。

app/welcome/welcome.component.spec.ts (class-only setup)

beforeEach(() => {
  TestBed.configureTestingModule({
    // provide the component-under-test and dependent service
    providers: [
      WelcomeComponent,
      { provide: UserService, useClass: MockUserService }
    ]
  });
  // inject both the component and the dependent service.
  comp = TestBed.get(WelcomeComponent);
  userService = TestBed.get(UserService);
});

次に、コンポーネントクラスを実行し、ライフサイクルフックメソッドをAngularがアプリケーションの実行時に呼び出すことを覚えておいてください。

app/welcome/welcome.component.spec.ts (class-only tests)

it('should not have welcome message after construction', () => {
  expect(comp.welcome).toBeUndefined();
});

it('should welcome logged in user after Angular calls ngOnInit', () => {
  comp.ngOnInit();
  expect(comp.welcome).toContain(userService.user.name);
});

it('should ask user to log in if not logged in after ngOnInit', () => {
  userService.isLoggedIn = false;
  comp.ngOnInit();
  expect(comp.welcome).not.toContain(userService.user.name);
  expect(comp.welcome).toContain('log in');
});

Component DOM testing

コンポーネントクラスのテストは、サービスをテストするのと同じくらい簡単です。

しかし、コンポーネントはクラスだけではありません。 コンポーネントは、DOMや他のコンポーネントと対話します。 クラスのみのテストでは、クラスの動作について教えてくれます。 コンポーネントが適切にレンダリングされるか、ユーザーの入力とジェスチャーに応答するか、または親コンポーネントと子コンポーネントと統合するかどうかは、ユーザーからはわかりません。

上記のクラスのみのテストでは、コンポーネントが実際に画面上でどのように動作するかについての重要な質問に答えることはできません。

  • Lightswitch.clicked()はユーザーが呼び出せるようなものにバインドされていますか?
  • Lightswitch.messageは表示されていますか?
  • ユーザーは実際にDashboardHeroComponentによって表示されるヒーローを選択できますか?
  • 主人公の名前は、大文字で表示されますか?
  • WelcomeComponentのテンプレートによってウェルカムメッセージが表示されていますか?

これらは、上に示した単純なコンポーネントの問題ではないかもしれません。 しかし、多くのコンポーネントは、テンプレートに記述されているDOM要素と複雑なやり取りをしているため、コンポーネントの状態が変わるとHTMLが表示され消えます。

これらの種類の質問に答えるには、コンポーネントに関連付けられたDOM要素を作成する必要があります。DOMを調べて、コンポーネント状態が適切なタイミングで適切に表示されることを確認し、画面とのユーザー対話をシミュレートして、 コンポーネントが期待どおりに動作するようにします。

この種のテストを作成するには、TestBedのその他の機能と他のテストヘルパを使用します。

CLI-generated tests

CLIは、新しいコンポーネントの生成を要求すると、デフォルトで初期テストファイルを作成します。

たとえば、次のCLIコマンドは、app / bannerフォルダ(インラインのテンプレートとスタイルを含む)にBannerComponentを生成します。

ng generate component banner --inline-template --inline-style --module app

また、次のようなコンポーネントbanner-external.component.spec.tsの初期テストファイルを生成します。

app/banner/banner-external.component.spec.ts (initial)

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BannerComponent } from './banner.component';

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

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

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

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

Reduce the setup

このファイルの最後の3行だけが実際にコンポーネントをテストし、Angularがコンポーネントを作成できると主張します。

ファイルの残りの部分は、構成要素が相当なものに発展した場合に必要となるより高度なテストを予期する定型的なセットアップコードです。

以下のこれらの高度なテスト機能について学びます。 今のところ、このテストファイルをもっと管理しやすいサイズに大幅に縮小することができます:

app/banner/banner-initial.component.spec.ts (minimal)

describe('BannerComponent (minimal)', () => {
  it('should create', () => {
    TestBed.configureTestingModule({
      declarations: [ BannerComponent ]
    });
    const fixture = TestBed.createComponent(BannerComponent);
    const component = fixture.componentInstance;
    expect(component).toBeDefined();
  });
});

この例では、TestBed.configureTestingModuleに渡されたメタデータオブジェクトは、単にテストするコンポーネントBannerComponentを宣言します。

TestBed.configureTestingModule({
  declarations: [ BannerComponent ]
});

宣言したり、他のものをインポートする必要はありません。 デフォルトのテストモジュールは、@angular/platform-browserのBrowserModuleのようなものがあらかじめ設定されています。

後で、テストニーズに合わせて、インポート、プロバイダ、およびその他の宣言でTestBed.configureTestingModule() を呼び出します。 オプションのオーバーライドメソッドを使用すると、構成の側面をさらに細かく調整できます。

createComponent()

TestBedを設定したら、そのcreateComponent() メソッドを呼び出します。

const fixture = TestBed.createComponent(BannerComponent);

TestBed.createComponent() は、BannerComponentのインスタンスを作成し、対応する要素をテストランナーDOMに追加し、ComponentFixtureを返します。

createComponentの呼び出し後にTestBedを再構成しないでください。

createComponentメソッドは現在のTestBedDefinitionをフリーズさせ、それを閉じてさらに設定します。

configureTestingModule()、get()、override …メソッドのいずれも、TestBed設定メソッドを呼び出すことはできません。 試してみると、TestBedはエラーを投げます。

ComponentFixture

ComponentFixtureは、作成されたコンポーネントおよび対応する要素と対話するためのテストハーネスです。

フィクスチャを介してコンポーネントインスタンスにアクセスし、Jasmine の期待通りに存在することを確認します。

const component = fixture.componentInstance;
expect(component).toBeDefined();

beforeEach()

このコンポーネントが進化するにつれて、より多くのテストを追加します。 各テストでTestBed設定を複製するのではなく、リファクタを使用して設定をJasmine beforeEach() といくつかのサポート変数にプルします。

describe('BannerComponent (with beforeEach)', () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ BannerComponent ]
    });
    fixture = TestBed.createComponent(BannerComponent);
    component = fixture.componentInstance;
  });

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

次に、fixture.nativeElementからコンポーネントの要素を取得し、期待されるテキストを探すテストを追加します。

it('should contain "banner works!"', () => {
  const bannerElement: HTMLElement = fixture.nativeElement;
  expect(bannerElement.textContent).toContain('banner works!');
});

nativeElement

ComponentFixture.nativeElementの値には、任意の型があります。 後でDebugElement.nativeElementが出現し、それにも任意の型があります。

Angularは、コンパイル時にnativeElementのHTML要素の種類やHTML要素であるかどうかを知ることはできません。 アプリケーションは、サーバーやWebワーカーなどの非ブラウザプラットフォームで実行されている可能性があります。このプラットフォームでは、要素のAPIが減少しているか、まったく存在していない可能性があります。

このガイドのテストは、ブラウザで実行するように設計されているため、nativeElementの値は常にHTMLElementまたはその派生クラスの1つになります。

それが何らかのHTMLElementであることがわかっている場合は、標準のHTML querySelectorを使用して要素ツリーに深く入り込むことができます。

段落要素を取得し、バナーテキストを探すためにHTMLElement.querySelectorを呼び出す別のテストを次に示します。

it('should have <p> with "banner works!"', () => {
  const bannerElement: HTMLElement = fixture.nativeElement;
  const p = bannerElement.querySelector('p');
  expect(p.textContent).toEqual('banner works!');
});

DebugElement

Angular fixtureは、fixture.nativeElementを通じてコンポーネントの要素を直接提供します。

const bannerElement: HTMLElement = fixture.nativeElement;

これは実際には便利なメソッドで、fixture.debugElement.nativeElementとして実装されています。

const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;

要素へのこの迂回路には正当な理由があります。

nativeElementのプロパティは、実行時環境によって異なります。 これらのテストは、DOMを持たない、またはDOMエミュレーションがHTMLElement API全体をサポートしていないブラウザ以外のプラットフォームで実行することができます。

Angularは、サポートされているすべてのプラットフォームで安全に動作するよう、DebugElement抽象化に依存しています。 Angularは、HTML要素ツリーを作成する代わりに、ランタイムプラットフォームのネイティブ要素をラップするDebugElementツリーを作成します。 nativeElementプロパティはDebugElementを展開し、プラットフォーム固有の要素オブジェクトを返します。

このガイドのサンプルテストはブラウザでのみ実行されるように設計されているため、これらのテストのnativeElementは、常にテスト内で探索できる使い慣れたメソッドとプロパティを持つHTMLElementです。

以前のテストはfixture.debugElement.nativeElementで再実装されています:

it('should find the <p> with fixture.debugElement.nativeElement)', () => {
  const bannerDe: DebugElement = fixture.debugElement;
  const bannerEl: HTMLElement = bannerDe.nativeElement;
  const p = bannerEl.querySelector('p');
  expect(p.textContent).toEqual('banner works!');
});

DebugElementには、このガイドの他の部分で説明するように、テストに役立つ他のメソッドとプロパティがあります。

AngularコアライブラリからDebugElementシンボルをインポートします。

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

By.css()

このガイドのテストはすべてブラウザ上で実行されますが、一部のアプリケーションは、少なくとも一部のプラットフォームで異なるプラットフォームで動作することがあります。

たとえば、接続の悪いデバイスでアプリケーションをより早く起動させる戦略の一環として、コンポーネントをサーバー上で最初にレンダリングすることがあります。 サーバー側のレンダラーは、完全なHTML要素APIをサポートしていない可能性があります。 querySelectorをサポートしていない場合、前のテストは失敗する可能性があります。

DebugElementは、サポートされているすべてのプラットフォームで動作するクエリメソッドを提供します。 これらのクエリメソッドは、DebugElementツリーのノードが選択基準に一致するとtrueを返す述語関数を使用します。

ランタイムプラットフォームのライブラリからインポートされたByクラスの助けを借りて述語を作成します。 ブラウザプラットフォームのByのインポートは次のとおりです。

import { By } from '@angular/platform-browser';

次の例では、DebugElement.query()とブラウザのBy.cssメソッドを使用して前のテストを再実装しています。

it('should find the <p> with fixture.debugElement.query(By.css)', () => {
  const bannerDe: DebugElement = fixture.debugElement;
  const paragraphDe = bannerDe.query(By.css('p'));
  const p: HTMLElement = paragraphDe.nativeElement;
  expect(p.textContent).toEqual('banner works!');
});

注目すべきいくつかの観察:

  • By.css()静的メソッドは、標準のCSSセレクタを持つDebugElementノードを選択します。
  • クエリは、段落のDebugElementを返します。
  • 段落要素を取得するには、その結果を展開する必要があります。
  • CSSセレクタでフィルタリングし、ブラウザのネイティブ要素のプロパティのみをテストする場合、By.cssの手法は過度のものになる可能性があります。

次の一連のテストで示すように、querySelector()やquerySelectorAll()などの標準的なHTMLElementメソッドを使用してフィルタ処理する方が簡単で、より明確になることがよくあります。

Component Test Scenarios

以下のセクションでは、このガイドのほとんどを構成し、一般的なコンポーネントのテストシナリオについて説明します。

Component binding

現在のBannerComponentは、静的タイトルテキストをHTMLテンプレートに表示します。

いくつか変更を加えた後、BannerComponentは、このようなコンポーネントのtitleプロパティにバインドすることによって、動的なタイトルを表示します。

app/banner/banner.component.ts

@Component({
  selector: 'app-banner',
  template: '<h1>{{title}}</h1>',
  styles: ['h1 { color: green; font-size: 350%}']
})
export class BannerComponent {
  title = 'Test Tour of Heroes';
}

これは簡単なので、テストを追加して、コンポーネントが実際に正しいと思われる場所にコンテンツが表示されることを確認します。

Query for the <h1>

タイトルプロパティ補間バインディングをラップする <h1> 要素の値を検査する一連のテストを記述します。

beforeEachを更新して、その要素を標準HTML querySelectorで検索し、それをh1変数に割り当てます。

app/banner/banner.component.spec.ts (setup)

let component: BannerComponent;
let fixture:   ComponentFixture<BannerComponent>;
let h1:        HTMLElement;

beforeEach(() => {
  TestBed.configureTestingModule({
    declarations: [ BannerComponent ],
  });
  fixture = TestBed.createComponent(BannerComponent);
  component = fixture.componentInstance; // BannerComponent test instance
  h1 = fixture.nativeElement.querySelector('h1');
});

createComponent() does not bind data

最初のテストでは、画面にデフォルトのタイトルが表示されていることを確認したいと思います。 あなたの本能は、このように<h1>を直ちに検査するテストを書くことです:

it('should display original title', () => {
  expect(h1.textContent).toContain(component.title);
});

そのテストは次のメッセージで失敗します。

expected '' to contain 'Test Tour of Heroes'.

Angularが変更検出を実行するとバインディングが発生します。

プロダクションでは、Angularがコンポーネントを作成するか、ユーザーがキーストロークを入力するか、非同期アクティビティ(AJAXなど)が完了すると、変更検出が自動的に開始されます。

TestBed.createComponentは変更検出をトリガーしません。 改訂されたテストで確認された事実:

it('no title in the DOM after createComponent()', () => {
  expect(h1.textContent).toEqual('');
});

detectChanges()

fixture.detectChanges()を呼び出すことによって、データバインディングを実行するようにTestBedに指示する必要があります。 それでは、<h1>は予期されたタイトルを持っています。

it('should display original title after detectChanges()', () => {
  fixture.detectChanges();
  expect(h1.textContent).toContain(component.title);
});

遅延変化検出は意図的かつ有用である。 テスターは、Angularがデータバインディングを開始してライフサイクルフックを呼び出す前に、コンポーネントの状態を検査して変更する機会をテスターに与えます。

fixture.detectChanges()を呼び出す前にコンポーネントのtitleプロパティを変更する別のテストがあります。

it('should display a different test title', () => {
  component.title = 'Test Title';
  fixture.detectChanges();
  expect(h1.textContent).toContain('Test Title');
});

Automatic change detection

BannerComponentは、頻繁にdetectChangesを呼び出します。 いくつかのテスターは、Angularテスト環境が自動的に変更検出を実行することを好みます。

これはComponentFixtureAutoDetectプロバイダーでTestBedを設定することで可能です。 まず、テストユーティリティーライブラリーからインポートします。

app/banner/banner.component.detect-changes.spec.ts (import)

import { ComponentFixtureAutoDetect } from '@angular/core/testing';

次に、それをテストモジュール設定のプロバイダ配列に追加します。

app/banner/banner.component.detect-changes.spec.ts (AutoDetect)

TestBed.configureTestingModule({
  declarations: [ BannerComponent ],
  providers: [
    { provide: ComponentFixtureAutoDetect, useValue: true }
  ]
});

自動変更検出がどのように機能するかを示す3つのテストがあります。

app/banner/banner.component.detect-changes.spec.ts (AutoDetect Tests)

it('should display original title', () => {
  // Hooray! No `fixture.detectChanges()` needed
  expect(h1.textContent).toContain(comp.title);
});

it('should still see original title after comp.title change', () => {
  const oldTitle = comp.title;
  comp.title = 'Test Title';
  // Displayed title is old because Angular didn't hear the change :(
  expect(h1.textContent).toContain(oldTitle);
});

it('should display updated title after detectChanges', () => {
  comp.title = 'Test Title';
  fixture.detectChanges(); // detect changes explicitly
  expect(h1.textContent).toContain(comp.title);
});

最初のテストでは、自動変更検出の利点が示されています。

2回目と3回目のテストで重要な制限が明らかになりました。 Angular テスト環境では、テストでコンポーネントのタイトルが変更されたことがわかりません。 ComponentFixtureAutoDetectサービスは、約束の解決、タイマー、DOMイベントなどの非同期アクティビティに応答します。 ただし、コンポーネントプロパティの直接的な同期更新は不可視です。 このテストでは、fixture.detectChanges() を手動で呼び出して、変更検出の別のサイクルをトリガする必要があります。

テストフィクスチャが変更検出を実行するかどうかを知るのではなく、このガイドのサンプルは常にdetectChanges()を明示的に呼び出します。 detectChanges()を厳密に必要以上に頻繁に呼び出しても問題ありません。

Change an input value with dispatchEvent()

ユーザー入力をシミュレートするには、input要素を見つけてvalueプロパティを設定します。

fixture.detectChanges()を呼び出してAngularの変更検出をトリガーします。 しかし、本質的な中間段階があります。

Angularは、input要素のvalueプロパティを設定していることを認識していません。 dispatchEvent()を呼び出して要素の入力イベントを発生させるまで、そのプロパティは読み取られません。 次に、detectChanges()を呼び出します。

次の例は、正しいシーケンスを示しています。

app/hero/hero-detail.component.spec.ts (pipe test)

    1. it('should convert hero name to Title Case', () => {
    2.   // get the name's input and display elements from the DOM
    3.   const hostElement = fixture.nativeElement;
    4.   const nameInput: HTMLInputElement = hostElement.querySelector('input');
    5.   const nameDisplay: HTMLElement = hostElement.querySelector('span');
    6.  
    7.   // simulate user entering a new name into the input box
    8.   nameInput.value = 'quick BROWN  fOx';
    9.  
    10.   // dispatch a DOM event so that Angular learns of input value change.
    11.   nameInput.dispatchEvent(newEvent('input'));
    12.  
    13.   // Tell Angular to update the display binding through the title pipe
    14.   fixture.detectChanges();
    15.  
    16.   expect(nameDisplay.textContent).toBe('Quick Brown  Fox');
    17. });

Component with external files

上記のBannerComponentは、@Component.templateプロパティと@Component.stylesプロパティでそれぞれ指定されたインラインテンプレートとインラインCSSで定義されています。

多くのコンポーネントは、BannerComponentの以下のバリアントが行うように、@Component.templateUrlプロパティと@Component.styleUrlsプロパティでそれぞれ外部テンプレートと外部CSSを指定します。

app/banner/banner-external.component.ts (metadata)

@Component({
  selector: 'app-banner',
  templateUrl: './banner-external.component.html',
  styleUrls:  ['./banner-external.component.css']
})

この構文は、コンポーネントコンパイル時に外部ファイルを読み込むようにAngularコンパイラに指示します。

テストを実行する前にアプリをコンパイルするので、CLI ng testコマンドを実行すると問題はありません。

ただし、非CLI環境でテストを実行すると、このコンポーネントのテストが失敗することがあります。 たとえば、プランナーなどのWebコーディング環境でBannerComponentテストを実行すると、次のようなメッセージが表示されます。

Error: This test module uses the component BannerComponent 
which is using a "templateUrl" or "styleUrls", but they were never compiled. 
Please call "TestBed.compileComponents" before your test.

テスト中にランタイム環境がソースコードをコンパイルすると、このテストエラーメッセージが表示されます。

問題を解決するには、belowで説明するようにcompileComponents()を呼び出します。

Component with a dependency

コンポーネントにはしばしばサービスの依存関係があります。

WelcomeComponentは、ログインしたユーザーへのウェルカムメッセージを表示します。 ユーザーが注入されたUserServiceのプロパティに基づいているかどうかを知る:

app/welcome/welcome.component.ts

import { Component, OnInit } from '@angular/core';
import { UserService }       from '../model/user.service';

@Component({
  selector: 'app-welcome',
  template: '<h3 class="welcome"><i>{{welcome}}</i></h3>'
})
export class WelcomeComponent  implements OnInit {
  welcome: string;
  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.welcome = this.userService.isLoggedIn ?
      'Welcome, ' + this.userService.user.name : 'Please log in.';
  }
}

WelcomeComponentには、サービスとやり取りする決定ロジックがあり、このコンポーネントをテストに値するロジックにします。 specファイルapp/welcome/welcome.component.spec.tsのテストモジュールの設定は次のとおりです。

app/welcome/welcome.component.spec.ts

TestBed.configureTestingModule({
   declarations: [ WelcomeComponent ],
// providers:    [ UserService ]  // NO! Don't provide the real service!
                                  // Provide a test-double instead
   providers:    [ {provide: UserService, useValue: userServiceStub } ]
});

今回は、テスト中のコンポーネントを宣言することに加えて、プロバイダリストにUserServiceプロバイダを追加します。 しかし、実際のUserServiceではありません。

Provide service test doubles

テスト中のコンポーネントには、実際のサービスを注入する必要はありません。 実際には、テストの倍数(スタブ、フェイク、スパイ、またはモック)であれば通常はより良いです。 仕様の目的は、サービスではなくコンポーネントをテストすることであり、実際のサービスは問題になる可能性があります。

実際のUserServiceを注入することは悪夢になる可能性があります。 実際のサービスは、ユーザーにログイン資格情報を要求し、認証サーバーに到達しようとします。 これらの動作は傍受するのが難しい場合があります。 実際のUserServiceの代わりにテストダブルを作成して登録する方がはるかに簡単で安全です。

この特定のテストスイートは、WelcomeComponentとそのテストのニーズを満たすUserServiceの最小限のモックを提供します。

app/welcome/welcome.component.spec.ts

let userServiceStub: Partial<UserService>;

userServiceStub = {
  isLoggedIn: true,
  user: { name: 'Test User'}
};

Get injected service

テストでは、WelcomeComponentに注入された(スタブ)UserServiceへのアクセスが必要です。

Angularは階層的な注入システムを持っています。 TestBedによって作成されたルートインジェクタからコンポーネントツリーまで、複数のレベルのインジェクタがあります。

注入されたサービスを取得する最も安全な方法は、常に動作する方法で、テスト中のコンポーネントのインジェクタから取得することです。 コンポーネントインジェクタは、フィクスチャのDebugElementのプロパティです。

WelcomeComponent’s injector

// UserService actually injected into the component
userService = fixture.debugElement.injector.get(UserService);

TestBed.get()

TestBed.get()経由でルートインジェクタからサービスを取得することもできます。 これは覚えやすく、あまり冗長ではありません。 しかし、Angularがコンポーネントのインスタンスをテストのルートインジェクタに注入する場合にのみ機能します。

このテストスイートでは、UserServiceの唯一のプロバイダはルートテストモジュールなので、TestBed.get()を次のように呼び出すことは安全です。

TestBed injector

// UserService from the root injector
userService = TestBed.get(UserService);

TestBed.get()が機能しないユースケースについては、「 Override a component’s providers」のセクションを参照して、コンポーネントのインジェクタからサービスを取得する必要があるときと理由を説明します。

Always get the service from an injector

テスト本体にあるテストモジュールに提供されているuserServiceStubオブジェクトを参照しないでください。 それは動作しません! コンポーネントに注入されたuserServiceインスタンスは、完全に異なるオブジェクトであり、提供されたuserServiceStubのクローンです。

it('stub object and injected UserService should not be the same', () => {
  expect(userServiceStub === userService).toBe(false);

  // Changing the stub object has no effect on the injected service
  userServiceStub.isLoggedIn = false;
  expect(userService.isLoggedIn).toBe(true);
});

Final setup and tests

TestBed.get() を使ってbeforeEach() を完成させましょう:

app/welcome/welcome.component.spec.ts

let userServiceStub: Partial<UserService>;

beforeEach(() => {
  // stub UserService for test purposes
  userServiceStub = {
    isLoggedIn: true,
    user: { name: 'Test User'}
  };

  TestBed.configureTestingModule({
     declarations: [ WelcomeComponent ],
     providers:    [ {provide: UserService, useValue: userServiceStub } ]
  });

  fixture = TestBed.createComponent(WelcomeComponent);
  comp    = fixture.componentInstance;

  // UserService from the root injector
  userService = TestBed.get(UserService);

  //  get the "welcome" element by CSS selector (e.g., by class name)
  el = fixture.nativeElement.querySelector('.welcome');
});

いくつかのテストがあります:

app/welcome/welcome.component.spec.ts

it('should welcome the user', () => {
  fixture.detectChanges();
  const content = el.textContent;
  expect(content).toContain('Welcome', '"Welcome ..."');
  expect(content).toContain('Test User', 'expected name');
});

it('should welcome "Bubba"', () => {
  userService.user.name = 'Bubba'; // welcome message hasn't been shown yet
  fixture.detectChanges();
  expect(el.textContent).toContain('Bubba');
});

it('should request login if not logged in', () => {
  userService.isLoggedIn = false; // welcome message hasn't been shown yet
  fixture.detectChanges();
  const content = el.textContent;
  expect(content).not.toContain('Welcome', 'not welcomed');
  expect(content).toMatch(/log in/i, '"log in"');
});

最初は健全性テストです。 スタブされたUserServiceが呼び出され、動作していることを確認します。

ジャスミン・マッチャーに対する第2のパラメータ(例えば、「予想される名前」)は、オプションの故障ラベルである。 予想が失敗した場合、Jasmineディスプレイはこのラベルを期待失敗メッセージに付加します。 複数の期待値を持つ仕様では、何が間違っていて、どの期待値が失敗したかを明確にするのに役立ちます。

残りのテストは、サービスが異なる値を返すときにコンポーネントのロジックを確認します。 2番目のテストでは、ユーザー名の変更の影響を検証します。 3番目のテストでは、ログインしているユーザーがいない場合、コンポーネントが適切なメッセージを表示していることを確認します。

Component with async service

このサンプルでは、AboutComponentテンプレートはTwainComponentをホストします。 TwainComponentはMark Twainの引用符を表示します。

app/twain/twain.component.ts (template)

template: `
  <p class="twain"><i>{{quote | async}}</i></p>
  <button (click)="getQuote()">Next quote</button>
  <p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>`,

コンポーネントのquoteプロパティの値は、AsyncPipeを通過することに注意してください。 つまり、プロパティはPromiseまたはObservableのいずれかを返します。

この例では、TwainComponent.getQuote() メソッドは、quoteプロパティがObservableを返すことを示しています。

getQuote() {
  this.errorMessage = '';
  this.quote = this.twainService.getQuote().pipe(
    startWith('...'),
    catchError( (err: any) => {
      // Wait a turn because errorMessage already set once this turn
      setTimeout(() => this.errorMessage = err.message || err.toString());
      return of('...'); // reset message to placeholder
    })
  );

TwainComponentは、注入されたTwainServiceから引用符を取得します。 コンポーネントは、サービスが最初の引用符を返す前に、返されたObservableをプレースホルダ値( ‘…')で開始します。

catchErrorはサービスエラーを傍受し、エラーメッセージを作成し、成功チャネルのプレースホルダ値を返します。 同じ変更検出サイクルでそのメッセージを2回更新するのを避けるために、エラーメッセージを設定するには、チェックを待つ必要があります。

これらはすべてテストしたい機能です。

Testing with a spy

コンポーネントをテストするときは、サービスのパブリックAPIだけが重要です。 一般に、テスト自体はリモートサーバーを呼び出すべきではありません。 彼らはそのような呼び出しをエミュレートする必要があります。 このapp/twain/twain.component.spec.tsの設定は、これを行う1つの方法を示しています:

app/twain/twain.component.spec.ts (setup)

beforeEach(() => {
  testQuote = 'Test Quote';

  // Create a fake TwainService object with a `getQuote()` spy
  const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
  // Make the spy return a synchronous Observable with the test data
  getQuoteSpy = twainService.getQuote.and.returnValue( of(testQuote) );

  TestBed.configureTestingModule({
    declarations: [ TwainComponent ],
    providers:    [
      { provide: TwainService, useValue: twainService }
    ]
  });

  fixture = TestBed.createComponent(TwainComponent);
  component = fixture.componentInstance;
  quoteEl = fixture.nativeElement.querySelector('.twain');
});

spy に焦点を当てる。

// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
// Make the spy return a synchronous Observable with the test data
getQuoteSpy = twainService.getQuote.and.returnValue( of(testQuote) );

スパイは、getQuoteへの任意の呼び出しがテスト見積もりで観測値を受け取れるように設計されています。 実際のgetQuote()メソッドとは異なり、このスパイはサーバをバイパスし、その値がすぐに利用できる同期観測値を返します。

Observableが同期していても、このスパイで多くの有用なテストを書くことができます。

Synchronous tests

同期Observableの主な利点は、非同期プロセスを同期テストにすることができることです。

it('should show quote after component initialized', () => {
  fixture.detectChanges(); // onInit()

  // sync spy result shows testQuote immediately after init
  expect(quoteEl.textContent).toBe(testQuote);
  expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called');
});

スパイの結果が同期的に返されるため、getQuote()メソッドは、AngularがngOnInitを呼び出す最初の変更検出サイクルの直後に画面上のメッセージを更新します。

エラー・パスをテストするときにあなたはとてもラッキーです。 サービススパイはエラーを同期的に返しますが、コンポーネントメソッドはsetTimeout()を呼び出します。 このテストは、JavaScriptエンジンが少なくとも1回転以上待ってから値を取得できるようにする必要があります。 テストは非同期にする必要があります。

Async test with fakeAsync()

次のテストは、サービスがErrorObservableを返すときの予想される動作を確認します。

    1. it('should display error when TwainService fails', fakeAsync(() => {
    2.   // tell spy to return an error observable
    3.   getQuoteSpy.and.returnValue(
    4.     throwError('TwainService test failure'));
    5.  
    6.   fixture.detectChanges(); // onInit()
    7.   // sync spy errors immediately after init
    8.  
    9.   tick(); // flush the component's setTimeout()
    10.  
    11.   fixture.detectChanges(); // update errorMessage within setTimeout()
    12.  
    13.   expect(errorMessage()).toMatch(/test failure/, 'should display error');
    14.   expect(quoteEl.textContent).toBe('...', 'should show placeholder');
    15. }));

it() 関数は次の形式の引数を受け取ることに注意してください。

fakeAsync(() => { /* test body */ })`

fakeAsync関数は、特定のfakeAsyncテストゾーンでテスト本体を実行することによって、線形コーディングスタイルを有効にします。 テスト本体は同期しているように見えます。 Promise.then()のようなネストされた構文はなく、制御の流れを混乱させることはありません。

The tick() function (仮想)クロックを進めるには、tick() を呼び出さなければなりません。

tick() を呼び出すと、保留中のすべての非同期アクティビティが終了するまでの時間がシミュレートされます。 この場合、エラーハンドラのsetTimeout() を待機します。

ティック機能は、TestBedでインポートするAngularテストユーティリティの1つです。 これはfakeAsyncのコンパニオンであり、fakeAsync本体内でのみ呼び出すことができます。

Async obserbables

これらのテストのテストカバレッジに満足しているかもしれません。

しかし、あなたは本当のサービスがこのように振る舞わないという事実に悩まされるかもしれません。 実際のサービスは、要求をリモートサーバーに送信します。 サーバーは応答するのに時間がかかり、前の2つのテストのように応答がすぐに利用できなくなります。

このようなgetQuote()スパイから非同期観測を返すと、あなたのテストは実世界をより忠実に反映します。

// Simulate delayed observable values with the `asyncData()` helper
getQuoteSpy.and.returnValue(asyncData(testQuote));

Async obserbable helpers

Async Observableは、asyncDataヘルパーによって生成されました。asyncDataヘルパーは、自分で作成する必要があるユーティリティ関数です。 または、サンプルコードからこれをコピーすることもできます。

testing/async-observable-helpers.ts

/** Create async observable that emits-once and completes
 *  after a JS engine turn */
export function asyncData<T>(data: T) {
  return defer(() => Promise.resolve(data));
}

このヘルパーの観測結果は、JavaScriptエンジンの次のターンでデータ値を出力します。

RxJS defer()演算子は、観測値を返します。 プロミスかオブザーバブルのどちらかを返すファクトリ関数を取ります。 あるものがdeferのobservableにサブスクライブすると、そのファクトリで作成された新しいobservableにサブスクライバが追加されます。

defer()演算子は、HttpClientのようにPromise.resolve()を新しい観測値に変換して、一度放出して完了します。 購読者は、データ値を受け取った後、購読を解除されます。

非同期エラーを生成するための同様のヘルパーがあります。

/** Create async observable error that errors
 *  after a JS engine turn */
export function asyncError<T>(errorObject: any) {
  return defer(() => Promise.reject(errorObject));
}

More async tests

getQuote() パイが非同期オブザーバブルを返すようになったので、ほとんどのテストは非同期でなければなりません。

現実世界で期待されるデータフローを示すfakeAsync() テストがあります。

it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {
  fixture.detectChanges(); // ngOnInit()
  expect(quoteEl.textContent).toBe('...', 'should show placeholder');

  tick(); // flush the observable to get the quote
  fixture.detectChanges(); // update view

  expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
  expect(errorMessage()).toBeNull('should not show error');
}));

quote要素は、ngOnInit()の後にプレースホルダ値( ‘…')を表示することに注意してください。 最初の見積もりはまだ届いていません。

最初の見積もりをobservableからフラッシュするには、tick()を呼び出します。 次に、detectChanges()を呼び出して、Angularに画面を更新するように指示します。

次に、quote要素が予想されるテキストを表示することをアサートすることができます。

Async test with async()

fakeAsync()ユーティリティ関数にはいくつかの制限があります。 特に、テスト本体がXHR呼び出しを行う場合は動作しません。

テスト中のXHR呼び出しはまれであるため、一般的にfakeAsync()を使うことができます。 しかし、XHRを呼び出す必要がある場合は、async()について知りたいでしょう。

TestBed.compileComponents()メソッド(下記参照)は、 “ジャストインタイム"コンパイル時に外部テンプレートとcssファイルを読み込むためにXHRを呼び出します。 async()ユーティリティを使用してcompileComponents()を呼び出すテストを作成します。

以前のfakeAsync()テストは、async()ユーティリティで再記述したものです。

it('should show quote after getQuote (async)', async(() => {
  fixture.detectChanges(); // ngOnInit()
  expect(quoteEl.textContent).toBe('...', 'should show placeholder');

  fixture.whenStable().then(() => { // wait for async getQuote
    fixture.detectChanges();        // update view with quote
    expect(quoteEl.textContent).toBe(testQuote);
    expect(errorMessage()).toBeNull('should not show error');
  });
}));

async()ユーティリティは、テスターのコードを特別な非同期テストゾーンで実行するように設定することによって、非同期ボイラープレートを非表示にします。 Jasmineのdone()をテストに渡す必要はなく、約束されたコールバックや観測可能なコールバックでdone()を呼び出す必要はありません。

しかし、テストの非同期性はfixture.whenStable()の呼び出しによって明らかになります。これは制御の線形フローを壊します。

whenStable

テストは、getQuote() observableが次のクォートを発行するのを待つ必要があります。 tick() を呼び出す代わりに、fixture.whenStable() を呼び出します。

fixture.whenStable() は、JavaScriptエンジンのタスクキューが空になったときに解決する約束を返します。 この例では、オブザーバブルが最初のクォートを発行すると、タスクキューは空になります。

テストは、promiseコールバック内で再開し、detectChanges() を呼び出してquote要素を期待されるテキストで更新します。

Jasmine done()

async関数とfakeAsync関数はAngular非同期テストを大幅に簡素化しますが、従来の技術に戻って、コールバックを完了させる関数を渡すことができます。

これで、約束事を連鎖させ、エラーを処理し、適切な時にdone() を呼び出す責任があります。

done() でテスト関数を書くことは、asyncとfakeAsyncよりも面倒です。 しかし時折必要です。 たとえば、intervalTimer() またはRxJS delay() 演算子を含むコードをテストするときは、asyncまたはfakeAsyncを呼び出すことはできません。

done() で書かれた前のテストの2つのムーバーバージョンがあります。 最初のコンポーネントは、コンポーネントのquoteプロパティによってテンプレートに公開されたObservableにサブスクライブします。

it('should show last quote (quote done)', (done: DoneFn) => {
  fixture.detectChanges();

  component.quote.pipe( last() ).subscribe(() => {
    fixture.detectChanges(); // update view with quote
    expect(quoteEl.textContent).toBe(testQuote);
    expect(errorMessage()).toBeNull('should not show error');
    done();
  });
});

RxJS last() 演算子は、完了する前に観測値の最後の値を出力します。これはテスト見積もりになります。 subscribeコールバックは、以前のテストと同じ方法で、クォート要素をテスト引用符で更新するためにdetectChanges() を呼び出します。

いくつかのテストでは、インジェクションされたサービスメソッドがどのように呼び出され、返された値が画面に表示されるかに、より関心があります。

偽のTwainServiceのqetQuote() スパイなどのサービススパイは、その情報を提供し、ビューの状態についてアサーションを行うことができます。

it('should show quote after getQuote (spy done)', (done: DoneFn) => {
  fixture.detectChanges();

  // the spy's most recent call returns the observable with the test quote
  getQuoteSpy.calls.mostRecent().returnValue.subscribe(() => {
    fixture.detectChanges(); // update view with quote
    expect(quoteEl.textContent).toBe(testQuote);
    expect(errorMessage()).toBeNull('should not show error');
    done();
  });
});

Component marble tests

以前のTwainComponentテストでは、asyncDataとasyncErrorユーティリティを使用して、TwainServiceからの非同期観測可能な応答をシミュレートしました。

これらはあなた自身で書くことができる簡単で簡単な機能です。 残念ながら、多くの一般的なシナリオでは単純すぎます。 可観測性はしばしば重大な遅延の後に、複数回出現する。 コンポーネントは、重複している値とエラーのシーケンスで複数のオブザーバブルを調整できます。

RxJS大理石テストは、シンプルかつ複雑な観測可能なシナリオをテストするうえで最適な方法です。 あなたは、観測可能物がどのように働くかを示す大理石図を見たことがあります。 大理石テストでは、同様の大理石言語を使用して、テストで観測可能なストリームと期待値を指定します。

次の例では、大理石テストを使用したTwainComponentテストの2つを再訪します。

まず、jasmine-marbles npmパッケージをインストールします。 次に、必要なシンボルをインポートします。

app/twain/twain.component.marbles.spec.ts (import marbles)

import { cold, getTestScheduler } from 'jasmine-marbles';

見積もりを取得するための完全なテストは次のとおりです。

it('should show quote after getQuote (marbles)', () => {
  // observable test quote value and complete(), after delay
  const q$ = cold('---x|', { x: testQuote });
  getQuoteSpy.and.returnValue( q$ );

  fixture.detectChanges(); // ngOnInit()
  expect(quoteEl.textContent).toBe('...', 'should show placeholder');

  getTestScheduler().flush(); // flush the observables

  fixture.detectChanges(); // update view

  expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
  expect(errorMessage()).toBeNull('should not show error');
});

ジャスミンテストは同期的であることに注意してください。 fakeAsync()はありません。 大理石テストは、テストスケジューラを使用して、同期テストにおける時間の経過をシミュレートします。

marble test の美しさは、観測可能なストリームの視覚的定義にあります。 このテストでは、3つのフレーム(—)を待ち、値(x)を出力し、完了(|)するコールドオブザーバブルを定義します。 2番目の引数では、値マーカー(x)を出力値(testQuote)にマップします。

const q$ = cold('---x|', { x: testQuote });

marble ライブラリは、対応する観測値を構築します。この観測値は、getQuoteスパイの戻り値として設定されます。

大理石の観測値をアクティブにする準備ができたら、TestSchedulerにこのような用意されたタスクのキューをフラッシュするように指示します。

getTestScheduler().flush(); // flush the observables

このステップは、以前のfakeAsync() およびasync() の例のtick() およびwhenStable() に似た目的を果たします。 テストのバランスは、それらの例と同じです。

Marble error testing

getQuote()エラーテストの marble テストバージョンです。

it('should display error when TwainService fails', fakeAsync(() => {
  // observable error after delay
  const q$ = cold('---#|', null, new Error('TwainService test failure'));
  getQuoteSpy.and.returnValue( q$ );

  fixture.detectChanges(); // ngOnInit()
  expect(quoteEl.textContent).toBe('...', 'should show placeholder');

  getTestScheduler().flush(); // flush the observables
  tick();                     // component shows error after a setTimeout()
  fixture.detectChanges();    // update error message

  expect(errorMessage()).toMatch(/test failure/, 'should display error');
  expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));

コンポーネント自体がエラーを処理するときにsetTimeout() を呼び出すため、fakeAsync() とtick() を呼び出すことはまだ非同期テストです。

marble observable の定義を見てください。

const q$ = cold('---#|', null, new Error('TwainService test failure'));

これは、3つのフレームを待ってからエラーを発するコールド・オブザーバブルです。ハッシュ(#)は、3番目の引数で指定されたエラーのタイミングを示します。 オブザーバブルが決して値を出力しないため、2番目の引数はnullです。

Laern about marble testing

marble フレームは、テスト時間の仮想単位です。 各記号( - 、x、|、#)は、1つのフレームの通過をマークします。

あなたがそれを購読するまで、冷たい観察可能値は値を生成しません。 あなたのアプリケーションのオブザーバブルのほとんどは寒いです。 すべてのHttpClient メソッドはコールドオブザーバブルを返します。

あなたがそれを購読する前に熱い観察可能なものがすでに価値を生み出しています。 ルーターの活動を報告するRouter.events observableは注目に値するものです。

RxJS marble テストは、このガイドの範囲を超えて、豊富なテーマです。 official documentationから始めて、ウェブ上で学んでください。

Component with inputs and outputs

入力と出力を持つコンポーネントは、通常、ホストコンポーネントのビューテンプレート内に表示されます。 ホストは、プロパティー・バインディングを使用して入力プロパティーを設定し、イベント・バインディングを使用して出力プロパティーによって発生したイベントをlistenします。

テストの目的は、そのようなバインディングが期待どおりに機能することを確認することです。 テストでは入力値を設定し、出力イベントを待機する必要があります。

DashboardHeroComponentは、このロール内のコンポーネントの小さな例です。 DashboardComponentによって提供される個々のヒーローを表示します。 そのヒーローをクリックすると、ユーザーがヒーローを選択したことをDashboardComponentに伝えます。

DashboardHeroComponentは、次のようにDashboardComponentテンプレートに埋め込まれています。

app/dashboard/dashboard.component.html (excerpt)

<dashboard-hero *ngFor="let hero of heroes"  class="col-1-4"
  [hero]=hero  (selected)="gotoDetail($event)" >
</dashboard-hero>

DashboardHeroComponentは* ngForリピータに表示され、各コンポーネントのヒーロー入力プロパティをループ値に設定し、コンポーネントの選択したイベントをリッスンします。

コンポーネントの完全な定義は次のとおりです。

app/dashboard/dashboard-hero.component.ts (component)

@Component({
  selector: 'dashboard-hero',
  template: `
    <div (click)="click()" class="hero">
      {{hero.name | uppercase}}
    </div>`,
  styleUrls: [ './dashboard-hero.component.css' ]
})
export class DashboardHeroComponent {
  @Input() hero: Hero;
  @Output() selected = new EventEmitter<Hero>();
  click() { this.selected.emit(this.hero); }
}

この単純なコンポーネントのテストは本質的な価値はほとんどありませんが、それを知ることは価値があります。 次のいずれかの方法を使用できます。

  • Test it as used by DashboardComponent.
  • Test it as a stand-alone component.
  • Test it as used by a substitute for DashboardComponent.

DashboardComponentコンストラクタを簡単に見てみると、最初のアプローチが難しくなります。

app/dashboard/dashboard.component.ts (constructor)

constructor(
  private router: Router,
  private heroService: HeroService) {
}

DashboardComponentはAngularルーターとHeroServiceによって異なります。 おそらくそれらを両方ともテストダブルと置き換える必要があります。これは多くの作業です。 ルータは特に難しいようです。

discussion below では、ルータを必要とするコンポーネントのテストについて説明します。

すぐに目標は、DashboardComponentではなくDashboardHeroComponentをテストすることです。したがって、2番目と3番目のオプションを試してみてください。

Test DashboardHeroComponent stand-alone

スペックファイルの設定は次のとおりです。

app/dashboard/dashboard-hero.component.spec.ts (setup)

TestBed.configureTestingModule({
  declarations: [ DashboardHeroComponent ]
})
fixture = TestBed.createComponent(DashboardHeroComponent);
comp    = fixture.componentInstance;

// find the hero's DebugElement and element
heroDe  = fixture.debugElement.query(By.css('.hero'));
heroEl = heroDe.nativeElement;

// mock the hero supplied by the parent component
expectedHero = { id: 42, name: 'Test Name' };

// simulate the parent setting the input property with that hero
comp.hero = expectedHero;

// trigger initial data binding
fixture.detectChanges();

セットアップコードがコンポーネントのヒーロープロパティにテストヒーロー(expectedHero)を割り当て、DashboardComponentがリピータのプロパティバインディングを介して設定する方法をエミュレートする方法に注意してください。

次のテストでは、ヒーロー名がバインディングを介してテンプレートに伝播することを確認します。

it('should display hero name in uppercase', () => {
  const expectedPipedName = expectedHero.name.toUpperCase();
  expect(heroEl.textContent).toContain(expectedPipedName);
});

テンプレートはヒーロー名をAngular UpperCasePipeで渡すので、テストでは要素の値と大文字の名前が一致する必要があります。

この小さなテストは、Angularテストがコンポーネントクラスのテストでは不可能だったコンポーネントのビジュアル表現を低コストで、はるかに遅く複雑なエンドツーエンドのテストに頼らずに検証する方法を示しています。

Clicking

ヒーローをクリックすると、ホストコンポーネント(おそらくDashboardComponent)が聞くことができる選択されたイベントが発生するはずです。

it('should raise selected event when clicked (triggerEventHandler)', () => {
  let selectedHero: Hero;
  comp.selected.subscribe((hero: Hero) => selectedHero = hero);

  heroDe.triggerEventHandler('click', null);
  expect(selectedHero).toBe(expectedHero);
});

コンポーネントの selectedプロパティはEventEmitterを返します。これは消費者にRxJS同期 Observableのように見えます。 テストは、ホストコンポーネントが暗黙的に行うのと同じように、明示的にサブスクライブします。 コンポーネントが期待どおりに動作する場合、ヒーローの要素をクリックすると、コンポーネントの selectedプロパティにheroオブジェクトを送出するよう指示する必要があります。 テストでは、「選択された」サブスクリプションを通じてそのイベントが検出されます。

triggerEventHandler

前のテストのheroDeは、ヒーロー

を表すDebugElementです。

Angularプロパティと、ネイティブ要素との相互作用を抽象化するメソッドがあります。 このテストでは、 “click"イベント名でDebugElement.triggerEventHandlerを呼び出します。 「クリック」イベントバインディングは、DashboardHeroComponent.click()を呼び出して応答します。

Angular DebugElement.triggerEventHandlerは、イベント名でデータバインドされたイベントを発生させることができます。 2番目のパラメータは、ハンドラに渡されるイベントオブジェクトです。

このテストでは、nullイベントオブジェクトを持つ「クリック」イベントがトリガされました。

heroDe.triggerEventHandler('click', null);

テストでは、ランタイムイベントハンドラ(コンポーネントのclick()メソッド)がイベントオブジェクトを気にかけていないことを前提としています(ここでは正しく)。

他のハンドラーはあまり寛容ではありません。 たとえば、RouterLinkディレクティブは、クリック中にどのマウスボタン(ある場合)が押されたのかを識別するbuttonプロパティを持つオブジェクトを想定しています。 イベントオブジェクトがない場合、RouterLinkディレクティブはエラーをスローします。

Click the element

次のテストの代替方法では、ネイティブエレメント自身のclick()メソッドが呼び出されます。このメソッドは、このコンポーネントにとっては問題ありません。

it('should raise selected event when clicked (element.click)', () => {
  let selectedHero: Hero;
  comp.selected.subscribe((hero: Hero) => selectedHero = hero);

  heroEl.click();
  expect(selectedHero).toBe(expectedHero);
});

Click helper

ボタン、アンカー、または任意のHTML要素をクリックすることは、一般的なテスト作業です。

以下のclick()関数のようなヘルパーにクリックトリガプロセスをカプセル化することで、一貫性があり簡単になります:

testing/index.ts (click helper)

/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */
export const ButtonClickEvents = {
   left:  { button: 0 },
   right: { button: 2 }
};

/** Simulate element click. Defaults to mouse left-button click event. */
export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
  if (el instanceof HTMLElement) {
    el.click();
  } else {
    el.triggerEventHandler('click', eventObj);
  }
}

最初のパラメータは要素からクリックまでです。 必要に応じて、カスタムイベントオブジェクトを2番目のパラメータとして渡すことができます。 デフォルトは、RouterLinkディレクティブを含む多くのハンドラで受け入れられる(一部の)左ボタンマウスイベントオブジェクトです。

click()ヘルパー関数はAngularテストユーティリティの1つではありません。 このガイドのサンプルコードで定義されている関数です。 すべてのサンプルテストで使用されています。 あなたが好きなら、あなた自身のヘルパーのコレクションに追加してください。

以前のテストは、クリックヘルパーを使って書き直しました。

app/dashboard/dashboard-hero.component.spec.ts (test with click helper)

it('should raise selected event when clicked (click helper)', () => {
  let selectedHero: Hero;
  comp.selected.subscribe(hero => selectedHero = hero);

  click(heroDe); // click helper with DebugElement
  click(heroEl); // click helper with native element

  expect(selectedHero).toBe(expectedHero);
});

Component inside a test host

以前のテストは、ホストDashboardComponent自身の役割を果たしました。 しかし、DashboardHeroComponentは、ホストコンポーネントに適切にデータバインドされていると正常に動作しますか?

実際のDashboardComponentでテストできます。 しかし、特に、そのテンプレートが* ngFor Repeater、他のコンポーネント、レイアウトHTML、追加のバインディング、複数のサービスを注入するコンストラクタを備えていて、それらのサービスとすぐにやりとりを開始するときに、多くの設定が必要になる可能性があります。

これらの注意散漫を無効にするための努力を想像してみましょう。これは、このようなテストホストで満足できるようにできる点を証明することです。

app/dashboard/dashboard-hero.component.spec.ts (test host)

@Component({
  template: `
    <dashboard-hero
      [hero]="hero" (selected)="onSelected($event)">
    </dashboard-hero>`
})
class TestHostComponent {
  hero: Hero = {id: 42, name: 'Test Name' };
  selectedHero: Hero;
  onSelected(hero: Hero) { this.selectedHero = hero; }
}

このテストホストは、DashboardComponentがDashboardHeroComponentにバインドしますが、ルータ、HeroService、または* ngForリピータのノイズはありません。

テストホストは、コンポーネントのヒーロー入力プロパティをテストヒーローに設定します。 コンポーネントのselectedイベントをonSelectedハンドラにバインドします。このハンドラは、selectedHeroプロパティに放出されたヒーローを記録します。

その後、テストではselectedHeroを簡単にチェックして、DashboardHeroComponent.selectedイベントが予想されるヒーローを発行したことを確認できます。

テストホストテストの設定は、スタンドアロンテストの設定と似ています。

app/dashboard/dashboard-hero.component.spec.ts (test host setup)

TestBed.configureTestingModule({
  declarations: [ DashboardHeroComponent, TestHostComponent ]
})
// create TestHostComponent instead of DashboardHeroComponent
fixture  = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
heroEl   = fixture.nativeElement.querySelector('.hero');
fixture.detectChanges(); // trigger initial data binding

このテストモジュールの構成には、3つの重要な違いがあります。

  1. DashboardHeroComponentとTestHostComponentの両方を宣言します。
  2. これは、DashboardHeroComponentの代わりにTestHostComponentを作成します。
  3. TestHostComponentはDashboardHeroComponent.heroにバインディングを設定します。
  4. createComponentは、DashboardHeroComponentのインスタンスの代わりにTestHostComponentのインスタンスを保持するフィクスチャを返します。

TestHostComponentを作成すると、前者のテンプレート内にDashboardHeroComponentが表示されるため、DashboardHeroComponentを作成するという副作用があります。 ヒーロー要素(heroEl)のクエリは、以前よりも要素ツリーの深さが深いものの、テストDOMでそれを検出します。

テスト自体は、スタンドアロンバージョンとほとんど同じです。

app/dashboard/dashboard-hero.component.spec.ts (test-host)

it('should display hero name', () => {
  const expectedPipedName = testHost.hero.name.toUpperCase();
  expect(heroEl.textContent).toContain(expectedPipedName);
});

it('should raise selected event when clicked', () => {
  click(heroEl);
  // selected hero should be the same data bound hero
  expect(testHost.selectedHero).toBe(testHost.hero);
});

選択したイベントテストのみが異なります。 これは、選択されたDashboardHeroComponentのヒーローが実際にホストコンポーネントへのイベントバインディングを通じてその方法を見つけることを確認します。

Routing component

ルーティングコンポーネントは、ルータに別のコンポーネントにナビゲートするように指示するコンポーネントです。 DashboardComponentは、ユーザーがダッシュボード上のヒーローボタンの1つをクリックしてHeroDetailComponentにナビゲートできるため、ルーティングコンポーネントです。

ルーティングはかなり複雑です。 DashboardComponentのテストは、HeroServiceと一緒に注入されるルータが関係しているため、部分的には難しいようでした。

app/dashboard/dashboard.component.ts (constructor)

constructor(
  private router: Router,
  private heroService: HeroService) {
}

HeroServiceをスパイで嘲笑するのはおなじみの話です。 しかし、ルータには複雑なAPIがあり、他のサービスやアプリケーションの前提条件と絡み合っています。 嘲笑するのは難しいかもしれませんか?

幸いなことに、このケースでは、DashboardComponentがルータであまり働いていないためです

app/dashboard/dashboard.component.ts (goToDetail)

gotoDetail(hero: Hero) {
  let url = `/heroes/${hero.id}`;
  this.router.navigateByUrl(url);
}

これはルーティングコンポーネントでよく発生します。 原則として、ルータではなくコンポーネントをテストし、指定された条件の下でコンポーネントが正しいアドレスでナビゲートする場合にのみ注意してください。

このコンポーネントテストスイートのためのルータースパイを提供することは、HeroServiceスパイを提供するのと同じくらい簡単です。

app/dashboard/dashboard.component.spec.ts (spies)

const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']);

TestBed.configureTestingModule({
  providers: [
    { provide: HeroService, useValue: heroServiceSpy },
    { provide: Router,      useValue: routerSpy }
  ]
})

次のテストでは、表示されたヒーローをクリックし、Router.navigateByUrlが期待されるURLで呼び出されたことを確認します。

app/dashboard/dashboard.component.spec.ts (navigate test)

it('should tell ROUTER to navigate when hero clicked', () => {

  heroClick(); // trigger click on first inner <div class="hero">

  // args passed to router.navigateByUrl() spy
  const spy = router.navigateByUrl as jasmine.Spy;
  const navArgs = spy.calls.first().args[0];

  // expecting to navigate to id of the component's first hero
  const id = comp.heroes[0].id;
  expect(navArgs).toBe('/heroes/' + id,
    'should nav to HeroDetail for first hero');
});

Routed components

ルーテッドコンポーネントはルーターナビゲーションの宛先です。 特にコンポーネントへのルートにパラメータが含まれている場合は、テストするのが難しい場合があります。 HeroDetailComponentは、そのようなルートの宛先であるルーティングされたコンポーネントです。

ユーザーがダッシュボードヒーローをクリックすると、DashboardComponentはルーターに英雄/:idにナビゲートするように指示します。 :idはルートパラメータで、その値は編集するヒーローのIDです。

ルーターはそのURLをHeroDetailComponentへのルートと照合します。 ルーティング情報を含むActivatedRouteオブジェクトを作成し、それをHeroDetailComponentの新しいインスタンスに挿入します。

HeroDetailComponentコンストラクタは次のとおりです。

app/hero/hero-detail.component.ts (constructor)

constructor(
  private heroDetailService: HeroDetailService,
  private route:  ActivatedRoute,
  private router: Router) {
}

HeroDetailコンポーネントはidパラメータを必要とするため、HeroDetailService経由で対応するヒーローを取得できます。 コンポーネントは、ObservableであるActivatedRoute.paramMapプロパティからIDを取得する必要があります。

ActivatedRoute.paramMapのidプロパティを参照するだけでは不十分です。 コンポーネントは、ActivatableRoute.paramMapオブザーバブルに登録し、IDがその存続期間中に変更されるように準備する必要があります。

app/hero/hero-detail.component.ts (ngOnInit)

ngOnInit(): void {
  // get hero when `id` param changes
  this.route.paramMap.subscribe(pmap => this.getHero(pmap.get('id')));
}

ルータガイドは、ActivatedRoute.paramMapについて詳しく説明しています。

テストでは、コンポーネントのコンストラクタに注入されたActivatedRouteを操作することによって、HeroDetailComponentが異なるidパラメータ値にどのように応答するかを調べることができます。

あなたはルータとデータサービスを偵察する方法を知っています。

あなたはActivatedRouteとは異なるアプローチをとるでしょう。

  • paramMapは、テスト中に複数の値を出力できるObservableを返します。
  • ParamMapを作成するには、ルーターヘルパー関数convertToParamMap()が必要です。
  • 他のルーティングコンポーネントテストでは、ActivatedRouteのテストダブルが必要です。

これらの違いは、再利用可能なスタブ・クラスを主張します。

ActivatedRouteStub

次のActivatedRouteStubクラスは、ActivatedRouteのテストdoubleとして機能します。

testing/activated-route-stub.ts (ActivatedRouteStub)

import { convertToParamMap, ParamMap, Params } from '@angular/router';
import { ReplaySubject } from 'rxjs';

/**
 * An ActivateRoute test double with a `paramMap` observable.
 * Use the `setParamMap()` method to add the next `paramMap` value.
 */
export class ActivatedRouteStub {
  // Use a ReplaySubject to share previous values with subscribers
  // and pump new values into the `paramMap` observable
  private subject = new ReplaySubject<ParamMap>();

  constructor(initialParams?: Params) {
    this.setParamMap(initialParams);
  }

  /** The mock paramMap observable */
  readonly paramMap = this.subject.asObservable();

  /** Set the paramMap observables's next value */
  setParamMap(params?: Params) {
    this.subject.next(convertToParamMap(params));
  };
}

このようなヘルパーをappフォルダの兄弟フォルダに配置することを検討してください。 このサンプルはActivatedRouteStubをtesting/activated-route-stub.tsに置きます。

marble テストライブラリでこのスタブクラスのより能力の高いバージョンを書くことを検討してください。

Testing with ActivatedRouteStub

観察されたIDが既存のヒーローを参照しているときのコンポーネントの動作を示すテストです:

app/hero/hero-detail.component.spec.ts (existing id)

describe('when navigate to existing hero', () => {
  let expectedHero: Hero;

  beforeEach(async(() => {
    expectedHero = firstHero;
    activatedRoute.setParamMap({ id: expectedHero.id });
    createComponent();
  }));

  it('should display that hero\'s name', () => {
    expect(page.nameDisplay.textContent).toBe(expectedHero.name);
  });
});

createComponent()メソッドとページオブジェクトについては後述します。 今あなたの直感に頼ってください。

idが見つからない場合、コンポーネントはHeroListComponentにリルートする必要があります。

テストスイートのセットアップは、実際にナビゲートせずにルータを偵察している上記の同じルータスパイを提供しました。

このテストでは、コンポーネントがHeroListComponentにナビゲートしようとします。

app/hero/hero-detail.component.spec.ts (bad id)

describe('when navigate to non-existent hero id', () => {
  beforeEach(async(() => {
    activatedRoute.setParamMap({ id: 99999 });
    createComponent();
  }));

  it('should try to navigate back to hero list', () => {
    expect(page.gotoListSpy.calls.any()).toBe(true, 'comp.gotoList called');
    expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
  });
});

このアプリはidパラメータを省略したHeroDetailComponentへのルートを持っていませんが、いつかそのようなルートを追加するかもしれません。 コンポーネントは、idがないときに妥当な何かを行うべきです。

この実装では、コンポーネントは新しいヒーローを作成して表示する必要があります。 新しいヒーローにはid = 0と空白の名前があります。 このテストは、コンポーネントが期待どおりに動作することを確認します。

app/hero/hero-detail.component.spec.ts (no id)

describe('when navigate with no hero id', () => {
  beforeEach(async( createComponent ));

  it('should have hero.id === 0', () => {
    expect(component.hero.id).toBe(0);
  });

  it('should display empty hero name', () => {
    expect(page.nameDisplay.textContent).toBe('');
  });
});

Nested component tests

コンポーネントテンプレートには、多くのコンポーネントが含まれるネストされたコンポーネントが含まれていることがよくあります。

コンポーネントツリーは非常に深くてもかまいません。ほとんどの場合、ネストされたコンポーネントは、ツリーの最上部にあるコンポーネントのテストには何の役割も果たしません。

たとえば、AppComponentは、アンカーとそのRouterLinkディレクティブを含むナビゲーションバーを表示します。

app/app.component.html

<app-banner></app-banner>
<app-welcome></app-welcome>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
  <a routerLink="/about">About</a>
</nav>
<router-outlet></router-outlet>

AppComponentクラスは空ですが、おそらく以下の理由で、リンクがRouterLinkディレクティブに正しく配線されているかどうかを確認するための単体テストを作成することができます。

リンクを検証するには、ルーターをナビゲートする必要はなく、ルーターがルーティングされたコンポーネントを挿入する場所を示すためには必要ありません。

BannerComponentとWelcomeComponent(で示される)も無関係です。

しかし、AppComponentをDOMに作成するテストでは、これらの3つのコンポーネントのインスタンスも作成されます。そのような場合は、TestBedを構成して作成する必要があります。

宣言を怠ると、AngularコンパイラはAppComponentテンプレートの、およびタグを認識せず、エラーをスローします。

実際のコンポーネントを宣言する場合は、ネストされたコンポーネントを宣言し、ツリー内の任意のコンポーネントに注入されたすべてのサービスも提供する必要があります。

これは、リンクについての簡単な質問に答えるだけの努力です。

このセクションでは、セットアップを最小限に抑えるための2つの手法について説明します。主要なコンポーネントのテストに集中するために、これらを単独または組み合わせて使用してください。

Stubbing unneeded components

最初の手法では、テストでほとんど役割を果たさないコンポーネントとディレクティブのスタブ・バージョンを作成して宣言します。

app/app.component.spec.ts (stub declaration)

@Component({selector: 'app-banner', template: ''})
class BannerStubComponent {}

@Component({selector: 'router-outlet', template: ''})
class RouterOutletStubComponent { }

@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}

スタブセレクタは、対応する実数成分のセレクタと一致します。 しかし、そのテンプレートとクラスは空です。

その後、TestBedコンフィグレーションで、実際に必要なコンポーネント、ディレクティブ、パイプの隣に宣言します。

app/app.component.spec.ts (TestBed stubs)

TestBed.configureTestingModule({
  declarations: [
    AppComponent,
    RouterLinkDirectiveStub,
    BannerStubComponent,
    RouterOutletStubComponent,
    WelcomeStubComponent
  ]
})

AppComponentはテスト対象ですので、もちろん実際のバージョンを宣言してください。

後で説明するRouterLinkDirectiveStubは、リンクテストに役立つ実際のRouterLinkのテストバージョンです。

残りはスタブです。

NO_ERRORS_SCHEMA

2番目の方法では、NO_ERRORS_SCHEMAをTestBed.schemasメタデータに追加します。

app/app.component.spec.ts (NO_ERRORS_SCHEMA)

TestBed.configureTestingModule({
  declarations: [
    AppComponent,
    RouterLinkDirectiveStub
  ],
  schemas: [ NO_ERRORS_SCHEMA ]
})

NO_ERRORS_SCHEMAは、認識できない要素と属性を無視するようにAngularコンパイラに指示します。

コンパイラは、TestBedコンフィグレーションで対応するAppComponentとRouterLinkDirectiveStubを宣言したため、要素とrouterLink属性を認識します。

しかし、、またはが見つかった場合、コンパイラはエラーを投げません。 単に空のタグとしてレンダリングし、ブラウザはそれらを無視します。

スタブコンポーネントはもう必要ありません。

Use both techniques together

これらは、コンポーネントの視覚的な表面をテストの対象となるコンポーネントのテンプレート内の要素だけに縮小するため、浅いコンポーネントテストの技術です。

NO_ERRORS_SCHEMAのアプローチは2つの方が簡単ですが、それを過度に使用しないでください。

また、NO_ERRORS_SCHEMAは、誤って省略した、またはスペルの間違ったコンポーネントや属性についてコンパイラーが知らせないようにします。 コンパイラが瞬時に捉えていたファントムバグを追跡する時間を無駄にすることがあります。

スタブコンポーネントアプローチには別の利点があります。 この例のスタブは空ですが、テストで何らかの方法でテストを行う必要がある場合は、テンプレートとクラスを取り除くことができます。

実際には、この例のように同じ設定で2つの手法を組み合わせます。

app/app.component.spec.ts (mixed setup)

TestBed.configureTestingModule({
  declarations: [
    AppComponent,
    BannerStubComponent,
    RouterLinkDirectiveStub
  ],
  schemas: [ NO_ERRORS_SCHEMA ]
})

Angularコンパイラは、<app-banner>要素のBannerComponentStubを作成し、routerLink属性を持つアンカーにRouterLinkStubDirectiveを適用しますが、<app-welcome>タグと<router-outlet>タグは無視します。

Components with RouterLink

実際のRouterLinkDirectiveはかなり複雑で、RouterModuleの他のコンポーネントとディレクティブと絡み合っています。 テストで模擬して使用するには、挑戦的なセットアップが必要です。

このサンプルコードのRouterLinkDirectiveStubは、実際のディレクティブを、AppComponentテンプレートに見られるアンカータグ配線の種類を検証するために設計された代替バージョンに置き換えます。

testing/router-link-directive-stub.ts (RouterLinkDirectiveStub)

@Directive({
  selector: '[routerLink]',
  host: { '(click)': 'onClick()' }
})
export class RouterLinkDirectiveStub {
  @Input('routerLink') linkParams: any;
  navigatedTo: any = null;

  onClick() {
    this.navigatedTo = this.linkParams;
  }
}

[routerLink]属性にバインドされたURLは、ディレクティブのlinkParamsプロパティに流れます。

ホストメタデータプロパティは、ホスト要素のクリックイベント(AppComponentの<a>アンカー要素)をスタブディレクティブのonClickメソッドに結び付けます。

アンカーをクリックすると、onClick()メソッドが起動し、スタブのtelltale navigatedToプロパティが設定されます。 テストでは、navigatedToを調べて、アンカーをクリックすると予想されるルート定義が設定されていることを確認します。

ルータがそのルート定義でナビゲートするように正しく設定されているかどうかは、別々のテストセットの質問です。

By.directive and injected directives

もう少しセットアップすると、最初のデータバインディングがトリガされ、ナビゲーションリンクへの参照が取得されます。

app/app.component.spec.ts (test setup)

beforeEach(() => {
  fixture.detectChanges(); // trigger initial data binding

  // find DebugElements with an attached RouterLinkStubDirective
  linkDes = fixture.debugElement
    .queryAll(By.directive(RouterLinkDirectiveStub));

  // get attached link directive instances
  // using each DebugElement's injector
  routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub));
});

特に興味のある3つのポイント:

  1. By.directiveを使用して、添付されたディレクティブでアンカー要素を見つけることができます。
  2. クエリは、一致する要素のまわりでDebugElementラッパーを返します。
  3. 各DebugElementは、その要素にアタッチされたディレクティブの特定のインスタンスを持つ依存インジェクタを公開します。

検証のためのAppComponentリンクは次のとおりです。

app/app.component.html (navigation links)

<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
  <a routerLink="/about">About</a>
</nav>

これらのリンクが期待通りにrouterLinkディレクティブに配線されていることを確認するテストがいくつかあります。

app/app.component.spec.ts (selected tests)

it('can get RouterLinks from template', () => {
  expect(routerLinks.length).toBe(3, 'should have 3 routerLinks');
  expect(routerLinks[0].linkParams).toBe('/dashboard');
  expect(routerLinks[1].linkParams).toBe('/heroes');
  expect(routerLinks[2].linkParams).toBe('/about');
});

it('can click Heroes link in template', () => {
  const heroesLinkDe = linkDes[1];   // heroes link DebugElement
  const heroesLink = routerLinks[1]; // heroes link directive

  expect(heroesLink.navigatedTo).toBeNull('should not have navigated yet');

  heroesLinkDe.triggerEventHandler('click', null);
  fixture.detectChanges();

  expect(heroesLink.navigatedTo).toBe('/heroes');
});

この例の「クリック」テストは誤解を招きます。 これは、コンポーネントではなく、RouterLinkDirectiveStubをテストします。 これは、ディレクティブスタブの一般的な失敗です。

このガイドには正当な目的があります。 ルータの全機能を使用することなく、RouterLink要素を見つけてクリックし、結果を検査する方法を示します。 これは、ユーザーがリンクをクリックしたときに、表示を変更したり、パラメータを再計算したり、ナビゲーションオプションを並べ替えたりする、より洗練されたコンポーネントをテストするために必要なスキルです。

What good are these tests?

Stubbed RouterLinkテストでは、リンクとコンセントを持つコンポーネントが正しく設定されていること、コンポーネントに必要なリンクがあり、すべてが期待される方向を指していることを確認できます。これらのテストは、ユーザーがリンクをクリックしたときにアプリケーションがターゲットコンポーネントにナビゲートするのに成功するかどうかには関係ありません。

このような制限されたテストの目的には、RouterLinkとRouterOutletをスタブすることが最良の選択肢です。実際のルータに依存すると、それらは脆弱になります。コンポーネントと無関係の理由で失敗する可能性があります。たとえば、ナビゲーションガードによって、権限のないユーザーがHeroListComponentにアクセスするのを防ぐことができます。これはAppComponentの欠陥ではなく、そのコンポーネントへの変更は失敗したテストを修正することはできません。

テストの別のバッテリーは、ユーザーが認証され、許可されているかどうかなど、ガードに影響する条件が存在する場合にアプリケーションが期待どおりにナビゲートするかどうかを調べることができます。

将来のガイドアップデートでは、RouterTestingModuleでこのようなテストを書く方法を説明します。

Use a page object

HeroDetailComponentは、タイトル、ヒーローフィールド2つ、ボタン2つのシンプルなビューです。

HeroDetailComponent in action

しかし、この単純な形式でも複雑なテンプレートがたくさんあります。

app/hero/hero-detail.component.html

<div *ngIf="hero">
  <h2><span>{{hero.name | titlecase}}</span> Details</h2>
  <div>
    <label>id: </label>{{hero.id}}</div>
  <div>
    <label for="name">name: </label>
    <input id="name" [(ngModel)]="hero.name" placeholder="name" />
  </div>
  <button (click)="save()">Save</button>
  <button (click)="cancel()">Cancel</button>
</div>

コンポーネントが必要とするテストが必要です…

  • 要素がDOMに現れる前にヒーローが到着するまで待つ。
  • タイトルテキストへの参照。
  • それを調べて設定するための名前入力ボックスへの参照。
  • 2つのボタンをクリックすることができるように2つのボタンへの参照。
  • いくつかのコンポーネントとルータの方法を偵察しています。

このような小さなフォームであっても、拷問された条件設定とCSS要素の選択が混乱することがあります。

コンポーネントプロパティへのアクセスを処理し、それらを設定するロジックをカプセル化するPageクラスを使用して、複雑さを克服してください。

hero-detail.component.spec.tsのページクラスです

app/hero/hero-detail.component.spec.ts (Page)

class Page {
  // getter properties wait to query the DOM until called.
  get buttons()     { return this.queryAll<HTMLButtonElement>('button'); }
  get saveBtn()     { return this.buttons[0]; }
  get cancelBtn()   { return this.buttons[1]; }
  get nameDisplay() { return this.query<HTMLElement>('span'); }
  get nameInput()   { return this.query<HTMLInputElement>('input'); }

  gotoListSpy: jasmine.Spy;
  navigateSpy:  jasmine.Spy;

  constructor(fixture: ComponentFixture<HeroDetailComponent>) {
    // get the navigate spy from the injected router spy object
    const routerSpy = <any> fixture.debugElement.injector.get(Router);
    this.navigateSpy = routerSpy.navigate;

    // spy on component's `gotoList()` method
    const component = fixture.componentInstance;
    this.gotoListSpy = spyOn(component, 'gotoList').and.callThrough();
  }

  //// query helpers ////
  private query<T>(selector: string): T {
    return fixture.nativeElement.querySelector(selector);
  }

  private queryAll<T>(selector: string): T[] {
    return fixture.nativeElement.querySelectorAll(selector);
  }
}

コンポーネントの操作と検査の重要なフックは、ページのインスタンスからきれいに整理され、アクセス可能になりました。

createComponentメソッドはページオブジェクトを作成し、ヒーローが到着すると空白を埋め込みます。

app/hero/hero-detail.component.spec.ts (createComponent)

/** Create the HeroDetailComponent, initialize it, set test variables  */
function createComponent() {
  fixture = TestBed.createComponent(HeroDetailComponent);
  component = fixture.componentInstance;
  page = new Page(fixture);

  // 1st change detection triggers ngOnInit which gets a hero
  fixture.detectChanges();
  return fixture.whenStable().then(() => {
    // 2nd change detection displays the async-fetched hero
    fixture.detectChanges();
  });
}

以前のセクションのHeroDetailComponentテストでは、createComponentとpageがテストを短くしてメッセージを保持する方法を示しています。 注意を払う必要はありません。約束を待つことなく、比較する要素値をDOMから検索する必要はありません。

この点を補強するHeroDetailComponentテストがいくつかあります。

app/hero/hero-detail.component.spec.ts (selected tests)

it('should display that hero\'s name', () => {
  expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});

it('should navigate when click cancel', () => {
  click(page.cancelBtn);
  expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
});

it('should save when click save but not navigate immediately', () => {
  // Get service injected into component and spy on its`saveHero` method.
  // It delegates to fake `HeroService.updateHero` which delivers a safe test result.
  const hds = fixture.debugElement.injector.get(HeroDetailService);
  const saveSpy = spyOn(hds, 'saveHero').and.callThrough();

  click(page.saveBtn);
  expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
  expect(page.navigateSpy.calls.any()).toBe(false, 'router.navigate not called');
});

it('should navigate when click save and save resolves', fakeAsync(() => {
  click(page.saveBtn);
  tick(); // wait for async save to complete
  expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
}));

it('should convert hero name to Title Case', () => {
  // get the name's input and display elements from the DOM
  const hostElement = fixture.nativeElement;
  const nameInput: HTMLInputElement = hostElement.querySelector('input');
  const nameDisplay: HTMLElement = hostElement.querySelector('span');

  // simulate user entering a new name into the input box
  nameInput.value = 'quick BROWN  fOx';

  // dispatch a DOM event so that Angular learns of input value change.
  nameInput.dispatchEvent(newEvent('input'));

  // Tell Angular to update the display binding through the title pipe
  fixture.detectChanges();

  expect(nameDisplay.textContent).toBe('Quick Brown  Fox');
});

Calling CompileComponents()

テストを実行する前にCLIがアプリケーションをコンパイルするので、CLI ng testコマンドでテストを実行するだけであれば、このセクションは無視できます。

非CLI環境でテストを実行すると、次のようなメッセージでテストが失敗することがあります。

Error: This test module uses the component BannerComponent 
which is using a "templateUrl" or "styleUrls", but they were never compiled. 
Please call "TestBed.compileComponents" before your test.

この問題の根本原因は、BannerComponentの次のバージョンのように、テストに関係するコンポーネントの少なくとも1つが外部テンプレートまたはCSSファイルを指定していることです。

app/banner/banner-external.component.ts (external template & css)

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

@Component({
  selector: 'app-banner',
  templateUrl: './banner-external.component.html',
  styleUrls:  ['./banner-external.component.css']
})
export class BannerComponent {
  title = 'Test Tour of Heroes';
}

TestBedがコンポーネントを作成しようとすると、テストは失敗します。

app/banner/banner.component.spec.ts (setup that fails)

beforeEach(() => {
  TestBed.configureTestingModule({
    declarations: [ BannerComponent ],
  });
  fixture = TestBed.createComponent(BannerComponent);
});

アプリがコンパイルされていないことを思い出してください。 したがって、createComponent()を呼び出すと、TestBedは暗黙的にコンパイルされます。

これは、ソースコードがメモリにあるときには問題ありません。 しかし、BannerComponentは、コンパイルが本質的に非同期操作であるファイルシステムから読み取らなければならない外部ファイルを必要とします。

TestBedを続行することが許されていれば、テストは実行され、コンパイラが終了する前に不思議に失敗します。

プリエンプティブエラーメッセージは、compileComponents()で明示的にコンパイルするよう指示します。

compileComponents() is async

非同期テスト関数内でcompileComponents()を呼び出す必要があります。

テスト機能を非同期にすることを怠った場合(たとえば、後述のasync()の使用を忘れた場合)、このエラーメッセージが表示されます Error: ViewDestroyedError: Attempt to use a destroyed view

典型的な方法は、セットアップロジックを2つの別々のbeforeEach()関数に分割することです。

  1. コンポーネントをコンパイルする非同期のbeforeEach()
  2. 残りのセットアップを実行する同期beforeEach()

このパターンに従うには、async()ヘルパーを他のテストシンボルとともにインポートします。

The async beforeEach

このように、最初の非同期beforeEachを記述します。

app/banner/banner-external.component.spec.ts (async beforeEach)

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [ BannerComponent ],
  })
  .compileComponents();  // compile template and css
}));

async()ヘルパー関数は、セットアップ本体にパラメータのない関数を取ります。

TestBed.configureTestingModule()メソッドはTestBedクラスを返します。これにより、compileComponents()などの他のTestBed静的メソッドに呼び出しをチェーンすることができます。

この例では、BannerComponentはコンパイルする唯一のコンポーネントです。 他の例では、テストモジュールを複数のコンポーネントで構成し、より多くのコンポーネントを保持するアプリケーションモジュールをインポートできます。 いずれも外部ファイルを必要とする可能性があります。

TestBed.compileComponentsメソッドは、テストモジュールで構成されたすべてのコンポーネントを非同期にコンパイルします。

compileComponents()の呼び出し後にTestBedを再構成しないでください。

compileComponents()を呼び出すと、現在のTestBedインスタンスが閉じられ、さらに設定が行われます。 configureTestingModule()やoverride …メソッドのいずれも呼び出すことはできません。 試してみると、TestBedはエラーを投げます。

compileComponents()をTestBed.createComponent()を呼び出す前の最後のステップにします。

The synchronous beforeEach

2番目の同期beforeEach()には、コンポーネントの作成と検査する要素のクエリを含む残りの設定手順が含まれています。

app/banner/banner-external.component.spec.ts (synchronous beforeEach)

beforeEach(() => {
  fixture = TestBed.createComponent(BannerComponent);
  component = fixture.componentInstance; // BannerComponent test instance
  h1 = fixture.nativeElement.querySelector('h1');
});

テストランナーは、最初の非同期beforeEachが完了してから2番目を呼び出すのを待つことができます。

Consolidated setup

2つのbeforeEach()関数を1つのasync beforeEach()に統合することができます。

compileComponents()メソッドは、同期コードをthen(…)コールバックに移動することによって、コンパイル後に同期セットアップタスクを実行できるように約束を返します。

app/banner/banner-external.component.spec.ts (one beforeEach)

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [ BannerComponent ],
  })
  .compileComponents()
  .then(() => {
    fixture = TestBed.createComponent(BannerComponent);
    component = fixture.componentInstance;
    h1 = fixture.nativeElement.querySelector('h1');
  });
}));

****compileComponents() is harmless

compileComponents()が必要でないときは、それを呼び出すことに害はありません。

CLIによって生成されたコンポーネントテストファイルは、ng testの実行時には必要ではないのにcompileComponents()を呼び出します。

このガイドのテストでは、必要に応じてcompileComponentsを呼び出すだけです。

Setup with module imports

以前のコンポーネントテストでは、次のようないくつかの宣言でテストモジュールを構成しました。

app/dashboard/dashboard-hero.component.spec.ts (configure TestBed)

TestBed.configureTestingModule({
  declarations: [ DashboardHeroComponent ]
})

DashboardComponentはシンプルです。 それは助けを必要としない。 しかし、より複雑なコンポーネントは、多くの場合、他のコンポーネント、ディレクティブ、パイプ、プロバイダに依存し、これらもテストモジュールに追加する必要があります。

幸いにも、TestBed.configureTestingModuleパラメータは、@ NgModuleデコレータに渡されるメタデータとパラレルになります。つまり、プロバイダとインポートを指定することもできます。

HeroDetailComponentは、サイズが小さく簡単な構成にもかかわらず、多くの助けが必要です。 デフォルトテストモジュールCommonModuleから受け取るサポートに加えて、次のものが必要です。

  • NgModelとFormsModule内の友達が双方向データバインディングを有効にします。
  • TitleCasePipeは、共有フォルダから取得します。
  • ルータサービス(これらのテストではスタブしています)。
  • ヒーローデータアクセスサービス(スタブされている)

1つの方法は、この例のようにテストモジュールを個々の部分から構成することです。

app/hero/hero-detail.component.spec.ts (FormsModule setup)

beforeEach(async(() => {
  const routerSpy = createRouterSpy();

  TestBed.configureTestingModule({
    imports:      [ FormsModule ],
    declarations: [ HeroDetailComponent, TitleCasePipe ],
    providers: [
      { provide: ActivatedRoute, useValue: activatedRoute },
      { provide: HeroService,    useClass: TestHeroService },
      { provide: Router,         useValue: routerSpy},
    ]
  })
  .compileComponents();
}));

beforeEach()は非同期で、TestBed.compileComponentsを呼び出します。これは、HeroDetailComponentに外部テンプレートとCSSファイルがあるためです。

上記のcompileComponents()の呼び出しで説明したように、これらのテストは、Angularがブラウザでコンパイルしなければならない非CLI環境で実行できます。

Import a shared module

多くのアプリコンポーネントはFormsModuleとTitleCasePipeを必要とするため、開発者は頻繁に要求されるこれらのコンポーネントと他の頻繁に要求されるコンポーネントを組み合わせるためにSharedModuleを作成しました。

テスト構成では、SharedModuleもこの代替設定で使用されるように使用できます。

app/hero/hero-detail.component.spec.ts (SharedModule setup)

beforeEach(async(() => {
  const routerSpy = createRouterSpy();

  TestBed.configureTestingModule({
    imports:      [ SharedModule ],
    declarations: [ HeroDetailComponent ],
    providers: [
      { provide: ActivatedRoute, useValue: activatedRoute },
      { provide: HeroService,    useClass: TestHeroService },
      { provide: Router,         useValue: routerSpy},
    ]
  })
  .compileComponents();
}));

インポートステートメントの数が少なくて済むようになっています(図示せず)。

Import a feature module

HeroDetailComponentはHeroModuleフィーチャモジュールの一部で、SharedModuleを含む相互依存関係の多くを集約します。 このようなHeroModuleをインポートするテスト設定を試してみましょう:

app/hero/hero-detail.component.spec.ts (HeroModule setup)

beforeEach(async(() => {
  const routerSpy = createRouterSpy();

  TestBed.configureTestingModule({
    imports:   [ HeroModule ],
    providers: [
      { provide: ActivatedRoute, useValue: activatedRoute },
      { provide: HeroService,    useClass: TestHeroService },
      { provide: Router,         useValue: routerSpy},
    ]
  })
  .compileComponents();
}));

それは本当に鮮明です。 プロバイダのテストは2倍にとどまります。 HeroDetailComponent宣言さえもなくなりました。

実際に、宣言しようとすると、HeroDetailComponentがTestBedによって作成されたHeroModuleとDynamicTestModuleの両方で宣言されるため、Angularはエラーをスローします。

フィーチャモジュールがそうであるように、コンポーネントのフィーチャモジュールをインポートすることは、モジュール内に相互依存関係が多く、モジュールが小さい場合にテストを構成する最も簡単な方法です。

Override component providers

HeroDetailComponentは独自のHeroDetailServiceを提供します。

app/hero/hero-detail.component.ts (prototype)

@Component({
  selector:    'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls:  ['./hero-detail.component.css' ],
  providers:  [ HeroDetailService ]
})
export class HeroDetailComponent implements OnInit {
  constructor(
    private heroDetailService: HeroDetailService,
    private route:  ActivatedRoute,
    private router: Router) {
  }
}

TestBed.configureTestingModuleのプロバイダでコンポーネントのHeroDetailServiceをスタブすることはできません。 それらはコンポーネントではなく、テストモジュールのプロバイダです。 彼らは、フィクスチャレベルで依存インジェクタを準備します。

Angularは、フィクスチャインジェクタの子である独自のインジェクタを使用してコンポーネントを作成します。 コンポーネントのプロバイダ(この場合はHeroDetailService)を子インジェクタに登録します。

テストでは、フィクスチャのインジェクタからのインジェクタサービスを受けることができません。 また、TestBed.configureTestingModuleはそれらを構成することもできません。

Angularは本物のHeroDetailServiceの新しいインスタンスを作成しています!

HeroDetailServiceがリモートサーバーへの独自のXHR呼び出しを行った場合、これらのテストは失敗するか、タイムアウトになる可能性があります。 呼び出すリモートサーバーがない可能性があります。

幸いにも、HeroDetailServiceは、注入されたHeroServiceへのリモートデータアクセスの責任を委任します。

app/hero/hero-detail.service.ts (prototype)

@Injectable()
export class HeroDetailService {
  constructor(private heroService: HeroService) {  }
/* . . . */
}

以前のテスト構成では、実際のHeroServiceが、サーバー要求をインターセプトして応答を偽装するTestHeroServiceに置き換えられました。

もしあなたがとても幸運でないなら、どうでしょうか? HeroServiceを捏造するのが難しい場合はどうすればいいですか? HeroDetailServiceが独自のサーバーリクエストを作成するとどうなりますか?

TestBed.overrideComponentメソッドを使用すると、コンポーネントのプロバイダを、以下のセットアップのバリエーションに示すように、管理しやすいテストダブルに置き換えることができます。

app/hero/hero-detail.component.spec.ts (Override setup)

beforeEach(async(() => {
  const routerSpy = createRouterSpy();

  TestBed.configureTestingModule({
    imports:   [ HeroModule ],
    providers: [
      { provide: ActivatedRoute, useValue: activatedRoute },
      { provide: Router,         useValue: routerSpy},
    ]
  })

  // Override component's own provider
  .overrideComponent(HeroDetailComponent, {
    set: {
      providers: [
        { provide: HeroDetailService, useClass: HeroDetailServiceSpy }
      ]
    }
  })

  .compileComponents();
}));

TestBed.configureTestingModuleは不要であるため、(偽の)HeroServiceを提供しなくなりました。

The overrideComponent method

overrideComponentメソッドに注目してください。

app/hero/hero-detail.component.spec.ts (overrideComponent)

.overrideComponent(HeroDetailComponent, {
  set: {
    providers: [
      { provide: HeroDetailService, useClass: HeroDetailServiceSpy }
    ]
  }
})

オーバーライドするコンポーネントタイプ(HeroDetailComponent)とオーバーライドメタデータオブジェクトの2つの引数をとります。 オーバーライドメタデータオブジェクトは、以下のように定義されたジェネリックです。

type MetadataOverride = {
    add?: T;
    remove?: T;
    set?: T;
  };

メタデータオーバーライドオブジェクトは、メタデータプロパティに要素を追加または削除するか、またはこれらのプロパティを完全にリセットすることができます。 この例では、コンポーネントのプロバイダのメタデータをリセットします。

型パラメータTは@Componentデコレータに渡すメタデータの種類です:

selector?: string;
template?: string;
templateUrl?: string;
providers?: any[];
...

Provide a spy stub (HeroDetailServiceSpy)

この例では、コンポーネントのproviders配列を、HeroDetailServiceSpyを含む新しい配列で完全に置き換えています。

HeroDetailServiceSpyは、そのサービスのすべての必要な機能を偽装する、実際のHeroDetailServiceのスタブ付きバージョンです。 下位レベルのHeroServiceにインジェクションもデリゲートもしないので、そのためのテストダブルを用意する必要はありません。

関連するHeroDetailComponentテストは、HeroDetailServiceのメソッドがサービスメソッドを偵察することによって呼び出されたことを宣言します。 したがって、スタブはそのメソッドをスパイとして実装します。

app/hero/hero-detail.component.spec.ts (HeroDetailServiceSpy)

class HeroDetailServiceSpy {
  testHero: Hero = {id: 42, name: 'Test Hero' };

  /* emit cloned test hero */
  getHero = jasmine.createSpy('getHero').and.callFake(
    () => asyncData(Object.assign({}, this.testHero))
  );

  /* emit clone of test hero, with changes merged in */
  saveHero = jasmine.createSpy('saveHero').and.callFake(
    (hero: Hero) => asyncData(Object.assign(this.testHero, hero))
  );
}

The override tests

テストでは、スパイスタブのtestHeroを操作してコンポーネントのヒーローを直接制御し、サービスメソッドが呼び出されたことを確認できます。

app/hero/hero-detail.component.spec.ts (override tests)

let hdsSpy: HeroDetailServiceSpy;

beforeEach(async(() => {
  createComponent();
  // get the component's injected HeroDetailServiceSpy
  hdsSpy = fixture.debugElement.injector.get(HeroDetailService) as any;
}));

it('should have called `getHero`', () => {
  expect(hdsSpy.getHero.calls.count()).toBe(1, 'getHero called once');
});

it('should display stub hero\'s name', () => {
  expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);
});

it('should save stub hero change', fakeAsync(() => {
  const origName = hdsSpy.testHero.name;
  const newName = 'New Name';

  page.nameInput.value = newName;
  page.nameInput.dispatchEvent(newEvent('input')); // tell Angular

  expect(component.hero.name).toBe(newName, 'component hero has new name');
  expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');

  click(page.saveBtn);
  expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once');

  tick(); // wait for async save to complete
  expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
  expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
}));

More overrides

TestBed.overrideComponentメソッドは、同じコンポーネントまたは異なるコンポーネントに対して複数回呼び出すことができます。 TestBedは、他のクラスの部分を掘り下げて置き換えるためのoverrideDirective、overrideModule、およびoverridePipeの同様のメソッドを提供します。

あなた自身のオプションと組み合わせを探そう。

Attribute Directive Testing

属性ディレクティブは、要素、コンポーネントまたは別のディレクティブの動作を変更します。 その名前は、ディレクティブがホスト・エレメントの属性として適用される方法を反映します。

サンプルアプリケーションのHighlightDirectiveは、データバインドされた色またはデフォルトの色(ライトグレー)のいずれかに基づいて要素の背景色を設定します。 また、要素(customProperty)のカスタムプロパティを、それが可能であることを示す以外の理由なしにtrueに設定します。

app/shared/highlight.directive.ts

import { Directive, ElementRef, Input, OnChanges } from '@angular/core';

@Directive({ selector: '[highlight]' })
/** Set backgroundColor for the attached element to highlight color
 *  and set the element's customProperty to true */
export class HighlightDirective implements OnChanges {

  defaultColor =  'rgb(211, 211, 211)'; // lightgray

  @Input('highlight') bgColor: string;

  constructor(private el: ElementRef) {
    el.nativeElement.style.customProperty = true;
  }

  ngOnChanges() {
    this.el.nativeElement.style.backgroundColor = this.bgColor || this.defaultColor;
  }
}

これは、アプリケーション全体で使用されています。おそらく、最も単純にAboutComponentにあります。

app/about/about.component.ts

import { Component } from '@angular/core';
@Component({
  template: `
  <h2 highlight="skyblue">About</h2>
  <h3>Quote of the day:</h3>
  <twain-quote></twain-quote>
  `
})
export class AboutComponent { }

AboutComponent内のHighlightDirectiveの特定の使用をテストするには、上記の手法(特に “浅いテスト"アプローチ)のみが必要です。

app/about/about.component.spec.ts

beforeEach(() => {
  fixture = TestBed.configureTestingModule({
    declarations: [ AboutComponent, HighlightDirective],
    schemas:      [ NO_ERRORS_SCHEMA ]
  })
  .createComponent(AboutComponent);
  fixture.detectChanges(); // initial binding
});

it('should have skyblue <h2>', () => {
  const h2: HTMLElement = fixture.nativeElement.querySelector('h2');
  const bgColor = h2.style.backgroundColor;
  expect(bgColor).toBe('skyblue');
});

しかし、単一のユースケースをテストすることは、ディレクティブの機能の全範囲を探索することはまずありません。 この指令を使用しているすべてのコンポーネントを見つけてテストするのは面倒で脆く、完全にカバーすることはほとんどありません。

クラスのみのテストは役に立ちますが、このような属性ディレクティブはDOMを操作する傾向があります。 隔離された単体テストはDOMに触れることはないので、指令の効力に対する信頼を促すものではありません。

より良い解決策は、指示を適用するすべての方法を示す人工的なテストコンポーネントを作成することです。

app/shared/highlight.directive.spec.ts (TestComponent)

@Component({
  template: `
  <h2 highlight="yellow">Something Yellow</h2>
  <h2 highlight>The Default (Gray)</h2>
  <h2>No Highlight</h2>
  <input #box [highlight]="box.value" value="cyan"/>`
})
class TestComponent { }

HighlightDirective spec in action

は、ハイライトディレクティブを入力ボックスのカラー値の名前にバインドします。 初期値は入力ボックスの背景色であるべき単語「シアン」である。

このコンポーネントのいくつかのテストがあります:

app/shared/highlight.directive.spec.ts (selected tests)

beforeEach(() => {
  fixture = TestBed.configureTestingModule({
    declarations: [ HighlightDirective, TestComponent ]
  })
  .createComponent(TestComponent);

  fixture.detectChanges(); // initial binding

  // all elements with an attached HighlightDirective
  des = fixture.debugElement.queryAll(By.directive(HighlightDirective));

  // the h2 without the HighlightDirective
  bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
});

// color tests
it('should have three highlighted elements', () => {
  expect(des.length).toBe(3);
});

it('should color 1st <h2> background "yellow"', () => {
  const bgColor = des[0].nativeElement.style.backgroundColor;
  expect(bgColor).toBe('yellow');
});

it('should color 2nd <h2> background w/ default color', () => {
  const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
  const bgColor = des[1].nativeElement.style.backgroundColor;
  expect(bgColor).toBe(dir.defaultColor);
});

it('should bind <input> background to value color', () => {
  // easier to work with nativeElement
  const input = des[2].nativeElement as HTMLInputElement;
  expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor');

  // dispatch a DOM event so that Angular responds to the input value change.
  input.value = 'green';
  input.dispatchEvent(newEvent('input'));
  fixture.detectChanges();

  expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');
});


it('bare <h2> should not have a customProperty', () => {
  expect(bareH2.properties['customProperty']).toBeUndefined();
});

いくつかの手法が注目に値する。

  • By.directive述部は、要素の型が不明な場合にこのディレクティブを持つ要素を取得するための優れた方法です。
  • :By.css( ‘h2:not([highlight])')の疑似クラスでは、ディレクティブを持たない <h2> 要素を見つけるのに役立ちます。 By.css( ‘*:not([highlight])')は、ディレクティブを持たない要素を検出します。
  • DebugElement.stylesは、DebugElement抽象化のおかげで、実際のブラウザがなくても要素スタイルにアクセスできます。 しかし、抽象化よりも簡単で明快な場合は、nativeElementを悪用しようと自由にしてください。
  • Angularは、それが適用されている要素のインジェクタにディレクティブを追加します。 デフォルトカラーのテストでは、2番目の <h2> のインジェクタを使用して、HighlightDirectiveインスタンスとdefaultColorを取得します。
  • DebugElement.propertiesは、ディレクティブによって設定された人工的なカスタムプロパティへのアクセスを提供します。

Pipe Testing

パイプは Angular テストユーティリティなしで簡単にテストできます。

パイプクラスには、入力値を変換された出力値に操作するtransformというメソッドがあります。 変換の実装は、DOMとほとんど対話しません。 ほとんどのパイプは@Pipeメタデータとインタフェース以外のAngularに依存しません。

各単語の最初の文字を大文字にするTitleCasePipeを考えてみましょう。 ここでは、正規表現を使った素朴な実装です。

app/shared/title-case.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({name: 'titlecase', pure: true})
/** Transform to Title Case: uppercase the first letter of the words in a string.*/
export class TitleCasePipe implements PipeTransform {
  transform(input: string): string {
    return input.length === 0 ? '' :
      input.replace(/\w\S*/g, (txt => txt[0].toUpperCase() + txt.substr(1).toLowerCase() ));
  }
}

正規表現を使用するものはすべて、十分にテストする価値があります。 シンプルなジャスミンを使用して、予想されるケースとエッジケースを調べます。

app/shared/title-case.pipe.spec.ts

describe('TitleCasePipe', () => {
  // This pipe is a pure, stateless function so no need for BeforeEach
  let pipe = new TitleCasePipe();

  it('transforms "abc" to "Abc"', () => {
    expect(pipe.transform('abc')).toBe('Abc');
  });

  it('transforms "abc def" to "Abc Def"', () => {
    expect(pipe.transform('abc def')).toBe('Abc Def');
  });

  // ... more tests ...
});

Write DOM tests too

これらは、パイプを単独でテストします。 TitleCasePipeがアプリケーションコンポーネントに正しく適用されているかどうかはわかりません。

このようなコンポーネントテストを追加することを検討してください。

app/hero/hero-detail.component.spec.ts (pipe test)

it('should convert hero name to Title Case', () => {
  // get the name's input and display elements from the DOM
  const hostElement = fixture.nativeElement;
  const nameInput: HTMLInputElement = hostElement.querySelector('input');
  const nameDisplay: HTMLElement = hostElement.querySelector('span');

  // simulate user entering a new name into the input box
  nameInput.value = 'quick BROWN  fOx';

  // dispatch a DOM event so that Angular learns of input value change.
  nameInput.dispatchEvent(newEvent('input'));

  // Tell Angular to update the display binding through the title pipe
  fixture.detectChanges();

  expect(nameDisplay.textContent).toBe('Quick Brown  Fox');
});

Test debugging

アプリケーションをデバッグするのと同じ方法で、ブラウザの仕様をデバッグします。

  1. カルマのブラウザウィンドウを表示します(前に隠れています)。
  2. DEBUGボタンをクリックします。 新しいブラウザタブを開き、テストを再実行します。
  3. ブラウザの「開発者ツール」(WindowsではCtrl-Shift-I、OSXではCommand-Option-I)を開きます。
  4. 「ソース」セクションを選択します。
  5. 1st.spec.tsテストファイル(Control / Command-Pを開き、ファイル名の入力を開始)を開きます。
  6. テストにブレークポイントを設定します。
  7. ブラウザを更新すると、ブレークポイントで停止します。

Karma debugging

Testing Utility APIs

このセクションでは、最も有用なAngularテスト機能のインベントリを取り上げ、その機能を要約します。

Angular testingユーティリティには、TestBed、ComponentFixture、およびテスト環境を制御するいくつかの関数が含まれています。 TestBedクラスとComponentFixtureクラスは別々に扱います。

スタンドアローン機能の概要をユーティリティーの順に示します。

Function Description
async 特別な非同期テストゾーン内でテスト(it)またはセットアップ(beforeEach)関数の本体を実行します。 上記の説明を参照してください。
fakeAsync 特殊なfakeAsyncテストゾーン内でテストの本体を実行し、リニアコントロールフローのコーディングスタイルを有効にします。 上記の説明を参照してください。
tick fakeAsyncテストゾーン内のタイマーとマイクロタスクキューの両方をフラッシュすることにより、時間の経過と非同期処理の完了をシミュレートします。

好奇心を持った読者は、この長いブログ記事「タスク、マイクロタスク、キュー、スケジュール」を楽しむかもしれません。

仮想クロックを指定されたミリ秒数だけ前進させ、その時間枠内でスケジュールされた非同期アクティビティをクリアするオプションの引数を受け入れます。 上記の説明を参照してください。
inject 現在のTestBedインジェクタから1つ以上のサービスをテスト機能に注入します。 コンポーネント自体によって提供されるサービスを注入することはできません。 debugElement.injectorの説明を参照してください。
discardPeriodicTasks fakeAsyncテストが保留中のタイマーイベントタスク(キューされたsetTimeOutおよびsetIntervalコールバック)で終了すると、テストは失敗し、クリアエラーメッセージが表示されます。

一般に、テストはキューに入れられたタスクなしで終了する必要があります。 保留中のタイマータスクが必要な場合は、discardPeriodicTasksを呼び出してタスクキューをフラッシュし、エラーを回避します。
flushMicrotasks fakeAsyncテストが未解決の約束などの保留中のマイクロタスクで終了すると、テストは失敗し、明確なエラーメッセージが表示されます。

一般に、テストはマイクロタスクが完了するのを待つべきです。 保留中のマイクロタスクが予想される場合は、flushMicrotasksを呼び出して、マイクロタスクキューをフラッシュし、エラーを回避します。
ComponentFixtureAutoDetect 自動変更検出を有効にするサービスのプロバイダトークン。
getTestBed TestBedの現在のインスタンスを取得します。 通常、TestBedクラスの静的クラスメソッドで十分です。 TestBedインスタンスは、ほとんど使用されない静的メソッドとして使用できるメンバーを公開します。

TestBed class summary

TestBedクラスは、主要なAngularテストユーティリティの1つです。 そのAPIは非常に大きく、あなたがそれを調べるまで圧倒的である可能性があります。 完全なAPIを吸収しようとする前に、まずこのガイドの最初の部分を読んで基礎を理解してください。

configureTestingModuleに渡されるモジュール定義は、@ NgModuleメタデータプロパティのサブセットです。

type TestModuleMetadata = {
  providers?: any[];
  declarations?: any[];
  imports?: any[];
  schemas?: Array<SchemaMetadata | any[]>;
};

各オーバーライドメソッドはMetadataOverride を取ります。ここで、Tはメソッドに適したメタデータの種類、つまり@NgModule、@Component、@Directive、または@Pipeのパラメータです。

type MetadataOverride = {
    add?: T;
    remove?: T;
    set?: T;
  };

TestBed APIは、theTestBedのグローバルインスタンスを更新または参照する静的クラスメソッドで構成されています。

内部的には、すべての静的メソッドは、現在のランタイムTestBedインスタンスのメソッドをカバーします。このメソッドは、getTestBed()関数によって返されます。

beforeEach()内のTestBedメソッドを呼び出して、個々のテストの前に新しい開始を確実にします。

ユーティリティの順に、最も重要な静的メソッドを次に示します。

Methods Description
configureTestingModule テストシム(karma-test-shim、browser-test-shim)は、初期テスト環境とデフォルトテストモジュールを確立します。 デフォルトのテストモジュールは、すべてのテスターが必要とする基本的な宣言といくつかのAngularサービスの代替で構成されています。

configureTestingModuleを呼び出すと、インポート、宣言(コンポーネント、ディレクティブ、パイプ)、およびプロバイダを追加および削除して、特定のテストセットのテストモジュール設定を絞り込むことができます。
compileComponents テストモジュールの設定が完了したら、テストモジュールを非同期でコンパイルします。 コンポーネントテンプレートとスタイルファイルの取得は必ず非同期であるため、テストモジュールコンポーネントのいずれかにtemplateUrlまたはstyleUrlsがある場合は、このメソッドを呼び出す必要があります。 上記を参照。

compileComponentsを呼び出した後、TestBed設定は現在の仕様の期間中フリーズされます。
createComponent 現在のTestBed構成に基づいて、タイプTのコンポーネントのインスタンスを作成します。 compileComponentを呼び出した後、TestBed設定は現在の仕様の期間中フリーズされます。
overrideModule 指定されたNgModuleのメタデータを置き換えます。 モジュールは他のモジュールをインポートできることを思い出してください。 overrideModuleメソッドは、これらの内部モジュールの1つを変更するために、現在のテストモジュールに深く到達することができます。
overrideComponent 指定されたコンポーネントクラスのメタデータを置き換えます。内側のモジュール内に深くネストすることができます。
overrideDirective 指定されたディレクティブクラスのメタデータを置き換えます。内部モジュールの内部に深くネストすることができます。
overridePipe 指定されたパイプクラスのメタデータを置き換えます。内側のモジュール内に深くネストすることができます。
get 現在のTestBedインジェクタからサービスを取得します。

注射機能は、この目的にはしばしば適切です。 しかし、注入がサービスを提供できない場合は、エラーをスローします。

サービスがオプションの場合はどうなりますか?

TestBed.get()メソッドはオプションの2番目のパラメータをとります.Angularがプロバイダを見つけることができない場合に返すオブジェクトです(この例ではnull)。

app/demo/demo.testbed.spec.ts service = TestBed.get(NotProvided, null);

getを呼び出した後、TestBed設定は現在の仕様の期間中フリーズします。
initTestEnvironment テスト実行全体のテスト環境を初期化します。

テストシム(カルマテストシム、ブラウザテストシム)はそれをあなたのために呼び出すので、あなたがそれを自分で呼び出す理由はめったにありません。

このメソッドを正確に1回呼び出すことができます。 テストの実行中にこのデフォルトを変更する必要がある場合は、最初にresetTestEnvironmentを呼び出します。

Angularコンパイラファクトリ、PlatformRef、およびデフォルトのAngularテストモジュールを指定します。 ブラウザ以外のプラットフォームの代替手段は、一般的な形式@ angular / platform- <platform_name> / testing / <platform_name>で利用できます。
resetTestEnvironment デフォルトテストモジュールを含む初期テスト環境をリセットします。

いくつかのTestBedインスタンスメソッドは静的なTestBedクラスメソッドの対象外です。 これらはめったに必要ありません。

The ComponentFixture

TestBed.createComponent は、コンポーネントTのインスタンスを作成し、そのコンポーネントに対して強く型付けされたComponentFixtureを返します。

ComponentFixtureのプロパティとメソッドは、コンポーネント、そのDOM表現、およびAngular環境の側面へのアクセスを提供します。

ComponentFixture properties

テスターの最も重要なプロパティは、ユーティリティの順になります。

Properties Description
componentInstance TestBed.createComponentによって作成されたコンポーネントクラスのインスタンスです。
debugElement コンポーネントのルート要素に関連付けられたDebugElement。

debugElementは、テストおよびデバッグ中に、コンポーネントとそのDOM要素を把握します。 これはテスターにとって重要な特性です。 最も興味深いのメンバーは以下でカバーされています。
nativeElement コンポーネントのルートにあるネイティブDOM要素。
changeDetectorRef コンポーネントのChangeDetectorRef。

ChangeDetectionRefは、ChangeDetectionStrategy.OnPushメソッドを持つコンポーネントをテストする場合や、コンポーネントの変更検出がプログラムによって制御されている場合に最も効果的です。

ComponentFixture methods

フィクスチャメソッドにより、Angularはコンポーネントツリー上で特定のタスクを実行します。 これらのメソッドを呼び出して、シミュレートされたユーザーアクションに応答して角度動作をトリガーします。

テスターにとって最も有用な方法は次のとおりです。

Methods Description
detectChanges コンポーネントの変更検出サイクルをトリガーします。

それを呼び出してコンポーネントを初期化し(ngOnInitを呼び出します)、テストコードの後にコンポーネントのデータバウンドプロパティ値を変更します。 Angularでは、personComponent.nameを変更したことを認識できません。また、detectChangesを呼び出すまで、名前のバインディングは更新されません。

detectChanges(false)と呼ばれない限り、checkNoChangesafterwardsを実行して循環更新がないことを確認します。
autoDetectChanges これをtrueに設定すると、フィクスチャーは自動的に変更を検出します。

自動検出が真の場合、テストフィクスチャはコンポーネントの作成直後にdetectChangesを呼び出します。 次に、関連するゾーンイベントをリッスンし、それに応じてdetectChangesを呼び出します。 テストコードがコンポーネントのプロパティ値を直接変更するときは、おそらくfixture.detectChangesを呼び出してデータバインディングの更新をトリガーする必要があります。

デフォルトはfalseです。 テストの動作を細かく制御することを好むテスタは、それを誤ったままにする傾向があります。
checkNoChanges 保留中の変更がないことを確認するために変更検出を実行します。 ある場合は例外をスローします。
isStable 治具が現在安定している場合はtrueを返します。 完了していない非同期タスクがある場合はfalseを返します。
whenStable 治具が安定しているときに解決する約束を返します。

非同期アクティビティまたは非同期変更検出が完了した後でテストを再開するには、その約束をフックします。 上記を参照。
destroy トリガーコンポーネントの破棄。

DebugElement

DebugElementは、コンポーネントのDOM表現に重要な洞察を提供します。

fixture.debugElementによって返されたテストルートコンポーネントのDebugElementから、フィクスチャの要素およびコンポーネントのサブツリー全体を歩く(およびクエリする)ことができます。

テスターの最も有用なDebugElementメンバーは、ユーティリティのおおよその順番です:

Member Description
nativeElement ブラウザの対応するDOM要素(WebWorkersの場合はnull)。
query 呼び出しクエリ(述語:述語)は、サブツリー内の任意の深さの述語に一致する最初のDebugElementを返します。
queryAll queryAll(述語:述語)を呼び出すと、サブツリー内の任意の深さの述語に一致するすべてのDebugElementsが返されます。
injector ホスト依存インジェクタ。 たとえば、ルート要素のコンポーネントインスタンスインジェクタなどです。
componentInstance 要素自身のコンポーネントインスタンス(存在する場合)。
context この要素の親コンテキストを提供するオブジェクト。 この要素を管理する祖先コンポーネントインスタンスです。

要素が* ngFor内で繰り返される場合、コンテキストは$ implicitプロパティが行インスタンス値の値であるNgForRowです。 例えば、* ngFor =の英雄は「英雄の英雄を放つ」。
children 即時のDebugElementの子です。 子供たちを降りて木を歩く。

DebugElementには、DebugNodeオブジェクトのリストであるchildNodesもあります。 DebugElementはDebugNodeオブジェクトから派生し、要素より多くのノードがあります。 テスターは通常、プレーンノードを無視できます。
parent DebugElementの親です。 これがルート要素の場合はnullです。
name 要素タグ名(要素の場合)。
triggerEventHandler 要素のリスナーコレクションに対応するリスナーがある場合、その名前でイベントをトリガーします。 2番目のパラメータは、ハンドラが予期するイベントオブジェクトです。 上記を参照。

イベントにリスナーがない場合やその他の問題がある場合は、nativeElement.dispatchEvent(eventObject)を呼び出すことを検討してください。
listeners コンポーネントの@Outputプロパティおよび/または要素のイベントプロパティに関連付けられたコールバック。
providerTokens このコンポーネントのインジェクタルックアップトークン。 コンポーネント自体とプロバイダのメタデータにコンポーネントがリストするトークンが含まれます。
source ソースコンポーネントテンプレートでこの要素を見つける場所。
references テンプレート変数(例:#foo)に関連付けられたオブジェクトの辞書。ローカル変数名をキーとしています。

DebugElement.query(predicate)およびDebugElement.queryAll(predicate)メソッドは、DebugElementと一致するようにソース要素のサブツリーをフィルタする述語を取ります。

述語は、DebugElementをとり、真理値を返す任意のメソッドです。 次の例では、 “content"という名前のテンプレートローカル変数への参照を含むすべてのDebugElementsが見つかります。

app/demo/demo.testbed.spec.ts

// Filter for DebugElements with a #content reference
const contentRefs = el.queryAll( de => de.references['content']);

Angular Byクラスには、共通述語の静的メソッドが3つあります。

  • By.all - すべての要素を返します。
  • By.css(セレクタ) - 一致するCSSセレクタを持つ要素を返します。
  • By.directive(ディレクティブ) - ディレクティブクラスのインスタンスに一致する角度の要素を返します。

app/hero/hero-list.component.spec.ts

// Can find DebugElement either by css selector or by directive
const h2        = fixture.debugElement.query(By.css('h2'));
const directive = fixture.debugElement.query(By.directive(HighlightDirective));

Frequently Asked Questions

Why put spec file next to the file it tests?

単体テスト仕様ファイルは、テストするアプリケーションソースコードファイルと同じフォルダに置くことをお勧めします。

  • そのようなテストは簡単に見つけることができます。
  • アプリケーションの一部にテストがないかどうか一目で分かります。
  • 近くのテストでは、部品がどのようにコンテキスト内で動作するかを明らかにすることができます。
  • あなたがソースを(必然的に)移動するときは、テストを移動することを忘れないでください。
  • ソースファイルの名前を変更する際には、テストファイルの名前を忘れずに変更してください。

When would I put specs in a test folder?

アプリケーション統合仕様では、フォルダやモジュールに分散された複数のパーツの相互作用をテストできます。 彼らは本当に特にどの部分にも属していないので、1つのファイルの隣に自然の家がありません。

testsディレクトリに適切なフォルダを作成する方がよい場合があります。

もちろん、テストヘルパーをテストするスペックは、対応するヘルパーファイルの隣のテストフォルダに属します。

Why not rely on E2E tests of DOM integration?

このガイドで説明されているコンポーネントのDOMテストでは、クラスのみのテストが比較的簡単なため、広範なセットアップと高度な技術が必要になることがよくあります。

なぜDOM統合テストをエンドツーエンド(E2E)テストに延期しないのですか?

E2Eテストは、システム全体の高レベル検証に最適です。しかし、ユニットテストで期待される包括的なテストカバレッジを与えることはできません。

E2Eテストは、単体テストに比べて書き込みや実行が難しいです。それらは容易に壊れます、頻繁に破損の場所から遠く離れた変化または不正行為のために。

E2Eテストでは、データの欠落や不良、接続の切断、リモートサービスの障害など、問題が発生したときにコンポーネントがどのように動作するかを簡単には明らかにできません。

データベースを更新したり、請求書を送信したり、クレジットカードに請求したりするアプリのE2Eテストでは、リモートリソースの偶発的な破損を防ぐために特殊なトリックとバックドアが必要です。テストしたいコンポーネントにナビゲートすることが難しい場合もあります。

これらの多くの障害のために、DOMテストの相互作用を可能な限りテストする必要があります。

agm 云々 Android のパケジ名変更メモ

comments powered by Disqus