このページでは、最も役立つAngularテスト機能について説明します。
Angularテストユーティリティには、TestBed
、ComponentFixture
、およびテスト環境を制御するいくつかの関数が含まれています。
TestBed
および ComponentFixture
クラスは、別途説明します。
以下は、スタンドアロン関数の概要を、ユーティリティの利用頻度順に示します。
関数 | 詳細 |
---|---|
waitForAsync |
テスト(it )または設定(beforeEach )関数の本体を、特別な 非同期テストゾーン 内で実行します。waitForAsync を参照してください。 |
fakeAsync |
テスト(it )関数の本体を、特別な fakeAsync テストゾーン 内で実行します。これにより、線形制御フローのコーディングスタイルが可能になります。fakeAsync を参照してください。 |
tick |
時間の経過と保留中の非同期アクティビティの完了をシミュレートし、fakeAsync テストゾーン 内の タイマー および マイクロタスク キューの両方をフラッシュします。興味のある読者向けに、この長いブログ投稿、タスク、マイクロタスク、キュー、スケジュールを紹介します。オプションの引数を受け付け、仮想クロックを指定されたミリ秒数だけ進めます。これにより、その時間枠内にスケジュールされた非同期アクティビティがクリアされます。tick を参照してください。 |
inject |
現在の TestBed インジェクターから、1 つ以上のサービスをテスト関数に注入します。これは、コンポーネント自体によって提供されるサービスを注入することはできません。debugElement.injector の議論を参照してください。 |
discardPeriodicTasks |
fakeAsync() テストが保留中のタイマーイベント タスク(キューに入れられた setTimeOut および setInterval コールバック)で終了すると、テストは明確なエラーメッセージで失敗します。 一般的に、テストはキューに入れられたタスクなしで終了する必要があります。保留中のタイマータスクが予想される場合は、 discardPeriodicTasks を呼び出して タスク キューをフラッシュし、エラーを回避します。 |
flushMicrotasks |
fakeAsync() テストが保留中の マイクロタスク(未解決の Promise など)で終了すると、テストは明確なエラーメッセージで失敗します。 一般的に、テストはマイクロタスクが完了するまで待つ必要があります。保留中のマイクロタスクが予想される場合は、 flushMicrotasks を呼び出して マイクロタスク キューをフラッシュし、エラーを回避します。 |
ComponentFixtureAutoDetect |
自動的な変更検出 をオンにするサービスの提供トークン。 |
getTestBed |
現在の TestBed のインスタンスを取得します。通常、TestBed クラスの静的クラスメソッドが十分なため、これは不要です。TestBed インスタンスは、静的メソッドとして利用できない、まれに使用されるメンバーをいくつか公開します。 |
TestBed
クラスの概要
TestBed
クラスは、主要なAngularテストユーティリティの1つです。
APIは非常に大きく、少しづつ調べていくまで、圧倒される可能性があります。
API全体を理解しようとする前に、このガイドの前半部分を読んで、基本を理解してください。
configureTestingModule
に渡されるモジュール定義は、@NgModule
メタデータプロパティのサブセットです。
type TestModuleMetadata = { providers?: any[]; declarations?: any[]; imports?: any[]; schemas?: Array<SchemaMetadata | any[]>;};
各オーバーライドメソッドは、MetadataOverride<T>
を受け取ります。ここで T
はメソッドに適したメタデータの種類、つまり @NgModule
、@Component
、@Directive
、または @Pipe
のパラメーターです。
type MetadataOverride<T> = { add?: Partial<T>; remove?: Partial<T>; set?: Partial<T>;};
TestBed
APIは、現在の TestBed
の グローバル インスタンスを更新または参照する静的クラスメソッドで構成されています。
内部的には、すべての静的メソッドは、現在のランタイム TestBed
インスタンスのメソッドをカバーしています。これは、getTestBed()
関数によっても返されます。
beforeEach()
内 で TestBed
メソッドを呼び出して、各テストの前に新しい開始を確保します。
以下は、最も重要な静的メソッドを、ユーティリティの利用頻度順に示します。
メソッド | 詳細 |
---|---|
configureTestingModule |
テストシム(karma-test-shim 、browser-test-shim )は、初期テスト環境 とデフォルトのテストモジュールを確立します。デフォルトのテストモジュールは、基本的な宣言と、すべてのテスターに必要な、いくつかの Angular サービスの代替で構成されています。 configureTestingModule を呼び出して、インポート、宣言(コンポーネント、ディレクティブ、およびパイプ)、およびプロバイダーを追加および削除することで、特定のテストセットのテストモジュール構成を洗練します。 |
compileComponents |
テストモジュールを構成し終えたら、非同期にコンパイルします。いずれかの テストモジュールコンポーネントに templateUrl または styleUrls がある場合は、このメソッドを 必ず 呼び出す必要があります。これは、コンポーネントテンプレートとスタイルファイルの取得が必ず非同期であるためです。compileComponents を参照してください。 compileComponents を呼び出すと、現在の仕様の期間中、TestBed の構成は固定されます。 |
createComponent<T> |
現在の TestBed の構成に基づいて、T 型のコンポーネントのインスタンスを作成します。createComponent を呼び出すと、現在の仕様の期間中、TestBed の構成は固定されます。 |
overrideModule |
指定された NgModule のメタデータを置き換えます。モジュールは他のモジュールをインポートできることに注意してください。overrideModule メソッドは、現在のテストモジュールを深く掘り下げて、これらの内部モジュールのいずれかを変更できます。 |
overrideComponent |
指定されたコンポーネントクラスのメタデータを置き換えます。これは、内部モジュールの中に深くネストされている可能性があります。 |
overrideDirective |
指定されたディレクティブクラスのメタデータを置き換えます。これは、内部モジュールの中に深くネストされている可能性があります。 |
overridePipe |
指定されたパイプクラスのメタデータを置き換えます。これは、内部モジュールの中に深くネストされている可能性があります。 |
inject |
現在の TestBed インジェクターからサービスを取得します。inject 関数は、この目的には多くの場合で十分です。ただし、inject は、サービスを提供できない場合にエラーをスローします。 サービスがオプションの場合どうすればよいですか? TestBed.inject() メソッドは、オプションの第 2 パラメーターとして、Angular がプロバイダーを見つけられない場合に返すオブジェクト(この例では null )を取ります。 TestBed.inject を呼び出すと、現在の仕様の期間中、TestBed の構成は固定されます。 |
initTestEnvironment |
テストの実行全体でテスト環境を初期化します。 テストシム( karma-test-shim 、browser-test-shim )はこれを実行するため、自分で呼び出す必要はほとんどありません。 このメソッドは ちょうど 1 回 呼び出します。テストの実行中にこのデフォルトを変更するには、最初に resetTestEnvironment を呼び出します。 Angular コンパイラーファクトリ、 PlatformRef 、およびデフォルトの Angular テストモジュールを指定します。ブラウザ以外のプラットフォームの代替手段は、@angular/platform-<platform_name>/testing/<platform_name> という一般的な形式で利用できます。 |
resetTestEnvironment |
デフォルトのテストモジュールを含む、初期テスト環境をリセットします。 |
TestBed
インスタンスのメソッドのいくつかは、静的な TestBed
クラス メソッドではカバーされていません。
これらは、めったに必要ありません。
ComponentFixture
TestBed.createComponent<T>
は、コンポーネント T
のインスタンスを作成し、そのコンポーネントの強く型付けされた ComponentFixture
を返します。
ComponentFixture
のプロパティとメソッドは、コンポーネント、そのDOM表現、およびAngular環境の側面へのアクセスを提供します。
ComponentFixture
のプロパティ
以下は、テスターにとって最も重要なプロパティを、ユーティリティの利用頻度順に示します。
プロパティ | 詳細 |
---|---|
componentInstance |
TestBed.createComponent によって作成されたコンポーネントクラスのインスタンス。 |
debugElement |
コンポーネントのルート要素に関連付けられた DebugElement 。 debugElement は、テストとデバッグ中に、コンポーネントとその DOM 要素に関する洞察を提供します。これは、テスターにとって重要なプロパティです。最も興味深いメンバーは、下記 に記載されています。 |
nativeElement |
コンポーネントのルートにあるネイティブ DOM 要素。 |
changeDetectorRef |
コンポーネントの ChangeDetectorRef 。 ChangeDetectorRef は、コンポーネントが ChangeDetectionStrategy.OnPush メソッドを持っているか、コンポーネントの変更検知がプログラムによって制御されている場合に最も役立ちます。 |
ComponentFixture
のメソッド
fixture メソッドは、Angularにコンポーネントツリーで特定のタスクを実行させます。 シミュレートされたユーザーアクションに応答して、Angularの動作をトリガーするには、これらのメソッドを呼び出します。
以下は、テスターにとって最も役立つメソッドです。
メソッド | 詳細 |
---|---|
detectChanges |
コンポーネントの変更検知サイクルをトリガーします。 コンポーネントを初期化するには、これを呼び出します( ngOnInit を呼び出します)。また、テストコードの後、コンポーネントのデータバインドプロパティ値を変更します。Angular は、personComponent.name を変更したことを認識していないため、detectChanges を呼び出すまで、name バインディングを更新しません。 detectChanges(false) として呼び出さない限り、後続で checkNoChanges を実行して、循環的な更新がないことを確認します。 |
autoDetectChanges |
true に設定すると、fixture が変更を自動的に検出します。 自動検出が true の場合、テスト fixture はコンポーネントの作成直後に detectChanges を呼び出します。その後、適切なゾーンイベントを監視し、それに応じて detectChanges を呼び出します。テストコードでコンポーネントのプロパティ値を直接変更する場合は、それでもデータバインディングの更新をトリガーするために、fixture.detectChanges を呼び出す必要がある可能性があります。 デフォルトは false です。テストの動作を細かく制御したいテスターは、通常、これを false のままにします。 |
checkNoChanges |
変更検知を実行して、保留中の変更がないことを確認します。変更がある場合は、例外をスローします。 |
isStable |
fixture が現在 安定 している場合は、true を返します。非同期タスクがまだ完了していない場合は、false を返します。 |
whenStable |
fixture が安定したら解決される Promise を返します。 非同期アクティビティまたは非同期的な変更検知の完了後にテストを再開するには、その Promise をフックします。whenStable を参照してください。 |
destroy |
コンポーネントの破棄をトリガーします。 |
DebugElement
DebugElement
は、コンポーネントのDOM表現に関する重要な洞察を提供します。
テストのルートコンポーネントの DebugElement
(fixture.debugElement
によって返される)から、fixtureの要素とコンポーネントのサブツリー全体を(クエリを使用して)移動できます。
以下は、テスターにとって最も役立つ DebugElement
のメンバーを、ユーティリティの利用頻度順に示します。
メンバー | 詳細 |
---|---|
nativeElement |
ブラウザの対応する DOM 要素 |
query |
query(predicate: Predicate<DebugElement>) を呼び出すと、サブツリーの任意の深さで、述語 と一致する最初の DebugElement が返されます。 |
queryAll |
queryAll(predicate: Predicate<DebugElement>) を呼び出すと、サブツリーの任意の深さで、述語 と一致するすべての DebugElements が返されます。 |
injector |
ホスト依存インジェクター。たとえば、ルート要素のコンポーネントインスタンスインジェクター。 |
componentInstance |
要素自身のコンポーネントインスタンス(存在する場合)。 |
context |
この要素に親コンテキストを提供するオブジェクト。多くの場合、この要素を管理する祖先コンポーネントインスタンスです。 要素が *ngFor 内で繰り返される場合、コンテキストは NgForOf で、その $implicit プロパティは行インスタンス値の値です。たとえば、*ngFor="let hero of heroes" の hero です。 |
children |
直近の DebugElement の子。children を介して階層を下降することで、ツリーを移動します。 DebugElement には childNodes もあり、これは DebugNode オブジェクトのリストです。DebugElement は DebugNode オブジェクトから派生しており、多くの場合、要素よりも多くのノードがあります。テスターは、通常、プレーンノードを無視できます。 |
parent |
DebugElement の親。これがルート要素の場合は null です。 |
name |
要素が要素の場合、要素のタグ名。 |
triggerEventHandler |
イベントに名前が付けられている場合、要素の listeners コレクションに対応するリスナーがある場合は、そのイベントをトリガーします。第 2 パラメーターは、ハンドラーで予想される イベントオブジェクト です。triggerEventHandler を参照してください。 イベントにリスナーがない場合や、その他の問題がある場合は、 nativeElement.dispatchEvent(eventObject) を呼び出すことを検討してください。 |
listeners |
コンポーネントの @Output プロパティまたは要素のイベントプロパティに添付されたコールバック。 |
providerTokens |
このコンポーネントのインジェクターのルックアップトークン。コンポーネント自体と、コンポーネントが providers メタデータにリストしているトークンが含まれます。 |
source |
ソースコンポーネントテンプレートでこの要素を見つける場所。 |
references |
テンプレートローカル変数(たとえば、#foo )に関連付けられているオブジェクトの辞書。ローカル変数名でキー付けされます。 |
DebugElement.query(predicate)
および DebugElement.queryAll(predicate)
メソッドは、ソース要素のサブツリーをフィルター処理して、一致する DebugElement
を見つける述語を受け取ります。
述語は、DebugElement
を受け取り、真偽値 を返す任意のメソッドです。
次の例は、"content" という名前のテンプレートローカル変数への参照を持つすべての DebugElements
を見つけます。
app/demo/demo.testbed.spec.ts
import {Component, DebugElement, Injectable} from '@angular/core';import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync,} from '@angular/core/testing';import {FormsModule, NgControl, NgModel} from '@angular/forms';import {By} from '@angular/platform-browser';import {addMatchers, click} from '../../testing';import { BankAccountComponent, BankAccountParentComponent, Child1Component, Child2Component, Child3Component, ExternalTemplateComponent, InputComponent, IoComponent, IoParentComponent, LightswitchComponent, MasterService, MyIfChildComponent, MyIfComponent, MyIfParentComponent, NeedsContentComponent, ParentComponent, ReversePipeComponent, ShellComponent, TestProvidersComponent, TestViewProvidersComponent, ValueService,} from './demo';export class NotProvided extends ValueService { /* example below */}beforeEach(addMatchers);describe('demo (with TestBed):', () => { //////// Service Tests ///////////// describe('ValueService', () => { let service: ValueService; beforeEach(() => { TestBed.configureTestingModule({providers: [ValueService]}); service = TestBed.inject(ValueService); }); it('should use ValueService', () => { service = TestBed.inject(ValueService); expect(service.getValue()).toBe('real value'); }); it('can inject a default value when service is not provided', () => { expect(TestBed.inject(NotProvided, null)).toBeNull(); }); it('test should wait for ValueService.getPromiseValue', waitForAsync(() => { service.getPromiseValue().then((value) => expect(value).toBe('promise value')); })); it('test should wait for ValueService.getObservableValue', waitForAsync(() => { service.getObservableValue().subscribe((value) => expect(value).toBe('observable value')); })); // Must use done. See https://github.com/angular/angular/issues/10127 it('test should wait for ValueService.getObservableDelayValue', (done: DoneFn) => { service.getObservableDelayValue().subscribe((value) => { expect(value).toBe('observable delay value'); done(); }); }); it('should allow the use of fakeAsync', fakeAsync(() => { let value: any; service.getPromiseValue().then((val: any) => (value = val)); tick(); // Trigger JS engine cycle until all promises resolve. expect(value).toBe('promise value'); })); }); describe('MasterService', () => { 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.inject(MasterService); valueServiceSpy = TestBed.inject(ValueService) as jasmine.SpyObj<ValueService>; }); it('#getValue should return stubbed value from a spy', () => { const stubValue = 'stub value'; valueServiceSpy.getValue.and.returnValue(stubValue); expect(masterService.getValue()).withContext('service returned stub value').toBe(stubValue); expect(valueServiceSpy.getValue.calls.count()) .withContext('spy method was called once') .toBe(1); expect(valueServiceSpy.getValue.calls.mostRecent().returnValue).toBe(stubValue); }); }); describe('use inject within `it`', () => { beforeEach(() => { TestBed.configureTestingModule({providers: [ValueService]}); }); it('should use modified providers', inject([ValueService], (service: ValueService) => { service.setValue('value modified in beforeEach'); expect(service.getValue()).toBe('value modified in beforeEach'); })); }); describe('using waitForAsync(inject) within beforeEach', () => { let serviceValue: string; beforeEach(() => { TestBed.configureTestingModule({providers: [ValueService]}); }); beforeEach(waitForAsync( inject([ValueService], (service: ValueService) => { service.getPromiseValue().then((value) => (serviceValue = value)); }), )); it('should use asynchronously modified value ... in synchronous test', () => { expect(serviceValue).toBe('promise value'); }); }); /////////// Component Tests ////////////////// describe('TestBed component tests', () => { // beforeEach(waitForAsync(() => { // TestBed.configureTestingModule() // // Compile everything in DemoModule // .compileComponents(); // })); it('should create a component with inline template', () => { const fixture = TestBed.createComponent(Child1Component); fixture.detectChanges(); expect(fixture).toHaveText('Child'); }); it('should create a component with external template', () => { const fixture = TestBed.createComponent(ExternalTemplateComponent); fixture.detectChanges(); expect(fixture).toHaveText('from external template'); }); it('should allow changing members of the component', () => { const fixture = TestBed.createComponent(MyIfComponent); fixture.detectChanges(); expect(fixture).toHaveText('MyIf()'); fixture.componentInstance.showMore = true; fixture.detectChanges(); expect(fixture).toHaveText('MyIf(More)'); }); it('should create a nested component bound to inputs/outputs', () => { const fixture = TestBed.createComponent(IoParentComponent); fixture.detectChanges(); const heroes = fixture.debugElement.queryAll(By.css('.hero')); expect(heroes.length).withContext('has heroes').toBeGreaterThan(0); const comp = fixture.componentInstance; const hero = comp.heroes[0]; click(heroes[0]); fixture.detectChanges(); const selected = fixture.debugElement.query(By.css('p')); expect(selected).toHaveText(hero.name); }); it('can access the instance variable of an `*ngFor` row component', () => { const fixture = TestBed.createComponent(IoParentComponent); const comp = fixture.componentInstance; const heroName = comp.heroes[0].name; // first hero's name fixture.detectChanges(); const ngForRow = fixture.debugElement.query(By.directive(IoComponent)); // first hero ngForRow const hero = ngForRow.context.hero; // the hero object passed into the row expect(hero.name).withContext('ngRow.context.hero').toBe(heroName); const rowComp = ngForRow.componentInstance; // jasmine.any is an "instance-of-type" test. expect(rowComp).withContext('component is IoComp').toEqual(jasmine.any(IoComponent)); expect(rowComp.hero.name).withContext('component.hero').toBe(heroName); }); it('should support clicking a button', () => { const fixture = TestBed.createComponent(LightswitchComponent); const btn = fixture.debugElement.query(By.css('button')); const span = fixture.debugElement.query(By.css('span')).nativeElement; fixture.detectChanges(); expect(span.textContent) .withContext('before click') .toMatch(/is off/i); click(btn); fixture.detectChanges(); expect(span.textContent).withContext('after click').toMatch(/is on/i); }); // ngModel is async so we must wait for it with promise-based `whenStable` it('should support entering text in input box (ngModel)', waitForAsync(() => { const expectedOrigName = 'John'; const expectedNewName = 'Sally'; const fixture = TestBed.createComponent(InputComponent); fixture.detectChanges(); const comp = fixture.componentInstance; const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; expect(comp.name) .withContext(`At start name should be ${expectedOrigName} `) .toBe(expectedOrigName); // wait until ngModel binds comp.name to input box fixture .whenStable() .then(() => { expect(input.value) .withContext( `After ngModel updates input box, input.value should be ${expectedOrigName} `, ) .toBe(expectedOrigName); // simulate user entering new name in input input.value = expectedNewName; // that change doesn't flow to the component immediately expect(comp.name) .withContext( `comp.name should still be ${expectedOrigName} after value change, before binding happens`, ) .toBe(expectedOrigName); // Dispatch a DOM event so that Angular learns of input value change. // then wait while ngModel pushes input.box value to comp.name input.dispatchEvent(new Event('input')); return fixture.whenStable(); }) .then(() => { expect(comp.name) .withContext(`After ngModel updates the model, comp.name should be ${expectedNewName} `) .toBe(expectedNewName); }); })); // fakeAsync version of ngModel input test enables sync test style // synchronous `tick` replaces asynchronous promise-base `whenStable` it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => { const expectedOrigName = 'John'; const expectedNewName = 'Sally'; const fixture = TestBed.createComponent(InputComponent); fixture.detectChanges(); const comp = fixture.componentInstance; const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; expect(comp.name) .withContext(`At start name should be ${expectedOrigName} `) .toBe(expectedOrigName); // wait until ngModel binds comp.name to input box tick(); expect(input.value) .withContext(`After ngModel updates input box, input.value should be ${expectedOrigName} `) .toBe(expectedOrigName); // simulate user entering new name in input input.value = expectedNewName; // that change doesn't flow to the component immediately expect(comp.name) .withContext( `comp.name should still be ${expectedOrigName} after value change, before binding happens`, ) .toBe(expectedOrigName); // Dispatch a DOM event so that Angular learns of input value change. // then wait a tick while ngModel pushes input.box value to comp.name input.dispatchEvent(new Event('input')); tick(); expect(comp.name) .withContext(`After ngModel updates the model, comp.name should be ${expectedNewName} `) .toBe(expectedNewName); })); it('ReversePipeComp should reverse the input text', fakeAsync(() => { const inputText = 'the quick brown fox.'; const expectedText = '.xof nworb kciuq eht'; const fixture = TestBed.createComponent(ReversePipeComponent); fixture.detectChanges(); const comp = fixture.componentInstance; const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement; // simulate user entering new name in input input.value = inputText; // Dispatch a DOM event so that Angular learns of input value change. // then wait a tick while ngModel pushes input.box value to comp.text // and Angular updates the output span input.dispatchEvent(new Event('input')); tick(); fixture.detectChanges(); expect(span.textContent).withContext('output span').toBe(expectedText); expect(comp.text).withContext('component.text').toBe(inputText); })); // Use this technique to find attached directives of any kind it('can examine attached directives and listeners', () => { const fixture = TestBed.createComponent(InputComponent); fixture.detectChanges(); const inputEl = fixture.debugElement.query(By.css('input')); expect(inputEl.providerTokens).withContext('NgModel directive').toContain(NgModel); const ngControl = inputEl.injector.get(NgControl); expect(ngControl).withContext('NgControl directive').toEqual(jasmine.any(NgControl)); expect(inputEl.listeners.length).withContext('several listeners attached').toBeGreaterThan(2); }); it('BankAccountComponent should set attributes, styles, classes, and properties', () => { const fixture = TestBed.createComponent(BankAccountParentComponent); fixture.detectChanges(); const comp = fixture.componentInstance; // the only child is debugElement of the BankAccount component const el = fixture.debugElement.children[0]; const childComp = el.componentInstance as BankAccountComponent; expect(childComp).toEqual(jasmine.any(BankAccountComponent)); expect(el.context).withContext('context is the child component').toBe(childComp); expect(el.attributes['account']).withContext('account attribute').toBe(childComp.id); expect(el.attributes['bank']).withContext('bank attribute').toBe(childComp.bank); expect(el.classes['closed']).withContext('closed class').toBe(true); expect(el.classes['open']).withContext('open class').toBeFalsy(); expect(el.styles['color']).withContext('color style').toBe(comp.color); expect(el.styles['width']) .withContext('width style') .toBe(comp.width + 'px'); // Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future? // expect(el.properties['customProperty']).toBe(true, 'customProperty'); }); }); describe('TestBed component overrides:', () => { it("should override ChildComp's template", () => { const fixture = TestBed.configureTestingModule({ imports: [Child1Component], }) .overrideComponent(Child1Component, { set: {template: '<span>Fake</span>'}, }) .createComponent(Child1Component); fixture.detectChanges(); expect(fixture).toHaveText('Fake'); }); it("should override TestProvidersComp's ValueService provider", () => { const fixture = TestBed.configureTestingModule({ imports: [TestProvidersComponent], }) .overrideComponent(TestProvidersComponent, { remove: {providers: [ValueService]}, add: {providers: [{provide: ValueService, useClass: FakeValueService}]}, // Or replace them all (this component has only one provider) // set: { providers: [{ provide: ValueService, useClass: FakeValueService }] }, }) .createComponent(TestProvidersComponent); fixture.detectChanges(); expect(fixture).toHaveText('injected value: faked value', 'text'); // Explore the providerTokens const tokens = fixture.debugElement.providerTokens; expect(tokens).withContext('component ctor').toContain(fixture.componentInstance.constructor); expect(tokens).withContext('TestProvidersComp').toContain(TestProvidersComponent); expect(tokens).withContext('ValueService').toContain(ValueService); }); it("should override TestViewProvidersComp's ValueService viewProvider", () => { const fixture = TestBed.configureTestingModule({ imports: [TestViewProvidersComponent], }) .overrideComponent(TestViewProvidersComponent, { // remove: { viewProviders: [ValueService]}, // add: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] // }, // Or replace them all (this component has only one viewProvider) set: {viewProviders: [{provide: ValueService, useClass: FakeValueService}]}, }) .createComponent(TestViewProvidersComponent); fixture.detectChanges(); expect(fixture).toHaveText('injected value: faked value'); }); it("injected provider should not be same as component's provider", () => { // TestComponent is parent of TestProvidersComponent @Component({ template: '<my-service-comp></my-service-comp>', imports: [TestProvidersComponent], }) class TestComponent {} // 3 levels of ValueService provider: module, TestComponent, TestProvidersComponent const fixture = TestBed.configureTestingModule({ imports: [TestComponent, TestProvidersComponent], providers: [ValueService], }) .overrideComponent(TestComponent, { set: {providers: [{provide: ValueService, useValue: {}}]}, }) .overrideComponent(TestProvidersComponent, { set: {providers: [{provide: ValueService, useClass: FakeValueService}]}, }) .createComponent(TestComponent); let testBedProvider!: ValueService; // `inject` uses TestBed's injector inject([ValueService], (s: ValueService) => (testBedProvider = s))(); const tcProvider = fixture.debugElement.injector.get(ValueService) as ValueService; const tpcProvider = fixture.debugElement.children[0].injector.get( ValueService, ) as FakeValueService; expect(testBedProvider).withContext('testBed/tc not same providers').not.toBe(tcProvider); expect(testBedProvider).withContext('testBed/tpc not same providers').not.toBe(tpcProvider); expect(testBedProvider instanceof ValueService) .withContext('testBedProvider is ValueService') .toBe(true); expect(tcProvider) .withContext('tcProvider is {}') .toEqual({} as ValueService); expect(tpcProvider instanceof FakeValueService) .withContext('tpcProvider is FakeValueService') .toBe(true); }); it('can access template local variables as references', () => { const fixture = TestBed.configureTestingModule({ imports: [ ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component, ], }) .overrideComponent(ShellComponent, { set: { selector: 'test-shell', imports: [NeedsContentComponent, Child1Component, Child2Component, Child3Component], template: ` <needs-content #nc> <child-1 #content text="My"></child-1> <child-2 #content text="dog"></child-2> <child-2 text="has"></child-2> <child-3 #content text="fleas"></child-3> <div #content>!</div> </needs-content> `, }, }) .createComponent(ShellComponent); fixture.detectChanges(); // NeedsContentComp is the child of ShellComp const el = fixture.debugElement.children[0]; const comp = el.componentInstance; expect(comp.children.toArray().length) .withContext('three different child components and an ElementRef with #content') .toBe(4); expect(el.references['nc']).withContext('#nc reference to component').toBe(comp); // Filter for DebugElements with a #content reference const contentRefs = el.queryAll((de) => de.references['content']); expect(contentRefs.length).withContext('elements w/ a #content reference').toBe(4); }); }); describe('nested (one-deep) component override', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ParentComponent, FakeChildComponent], }).overrideComponent(ParentComponent, { set: {imports: [FakeChildComponent]}, }); }); it('ParentComp should use Fake Child component', () => { const fixture = TestBed.createComponent(ParentComponent); fixture.detectChanges(); expect(fixture).toHaveText('Parent(Fake Child)'); }); }); describe('nested (two-deep) component override', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent], }).overrideComponent(ParentComponent, { set: {imports: [FakeChildWithGrandchildComponent, FakeGrandchildComponent]}, }); }); it('should use Fake Grandchild component', () => { const fixture = TestBed.createComponent(ParentComponent); fixture.detectChanges(); expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))'); }); }); describe('lifecycle hooks w/ MyIfParentComp', () => { let fixture: ComponentFixture<MyIfParentComponent>; let parent: MyIfParentComponent; let child: MyIfChildComponent; beforeEach(() => { TestBed.configureTestingModule({ imports: [FormsModule, MyIfChildComponent, MyIfParentComponent], }); fixture = TestBed.createComponent(MyIfParentComponent); parent = fixture.componentInstance; }); it('should instantiate parent component', () => { expect(parent).withContext('parent component should exist').not.toBeNull(); }); it('parent component OnInit should NOT be called before first detectChanges()', () => { expect(parent.ngOnInitCalled).toBe(false); }); it('parent component OnInit should be called after first detectChanges()', () => { fixture.detectChanges(); expect(parent.ngOnInitCalled).toBe(true); }); it('child component should exist after OnInit', () => { fixture.detectChanges(); getChild(); expect(child instanceof MyIfChildComponent) .withContext('should create child') .toBe(true); }); it("should have called child component's OnInit ", () => { fixture.detectChanges(); getChild(); expect(child.ngOnInitCalled).toBe(true); }); it('child component called OnChanges once', () => { fixture.detectChanges(); getChild(); expect(child.ngOnChangesCounter).toBe(1); }); it('changed parent value flows to child', () => { fixture.detectChanges(); getChild(); parent.parentValue = 'foo'; fixture.detectChanges(); expect(child.ngOnChangesCounter) .withContext('expected 2 changes: initial value and changed value') .toBe(2); expect(child.childValue).withContext('childValue should eq changed parent value').toBe('foo'); }); // must be async test to see child flow to parent it('changed child value flows to parent', waitForAsync(() => { fixture.detectChanges(); getChild(); child.childValue = 'bar'; return new Promise<void>((resolve) => { // Wait one JS engine turn! setTimeout(() => resolve(), 0); }).then(() => { fixture.detectChanges(); expect(child.ngOnChangesCounter) .withContext('expected 2 changes: initial value and changed value') .toBe(2); expect(parent.parentValue) .withContext('parentValue should eq changed parent value') .toBe('bar'); }); })); it('clicking "Close Child" triggers child OnDestroy', () => { fixture.detectChanges(); getChild(); const btn = fixture.debugElement.query(By.css('button')); click(btn); fixture.detectChanges(); expect(child.ngOnDestroyCalled).toBe(true); }); ////// helpers /// /** * Get the MyIfChildComp from parent; fail w/ good message if cannot. */ function getChild() { let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp // The Hard Way: requires detailed knowledge of the parent template try { childDe = fixture.debugElement.children[4].children[0]; } catch (err) { /* we'll report the error */ } // DebugElement.queryAll: if we wanted all of many instances: childDe = fixture.debugElement.queryAll( (de) => de.componentInstance instanceof MyIfChildComponent, )[0]; // WE'LL USE THIS APPROACH ! // DebugElement.query: find first instance (if any) childDe = fixture.debugElement.query( (de) => de.componentInstance instanceof MyIfChildComponent, ); if (childDe && childDe.componentInstance) { child = childDe.componentInstance; } else { fail('Unable to find MyIfChildComp within MyIfParentComp'); } return child; } });});////////// Fakes ///////////@Component({ selector: 'child-1', template: 'Fake Child',})class FakeChildComponent {}@Component({ selector: 'grandchild-1', template: 'Fake Grandchild',})class FakeGrandchildComponent {}@Component({ selector: 'child-1', imports: [FakeGrandchildComponent], template: 'Fake Child(<grandchild-1></grandchild-1>)',})class FakeChildWithGrandchildComponent {}@Injectable()class FakeValueService extends ValueService { override value = 'faked value';}
Angularの By
クラスには、一般的な述語の3つの静的メソッドがあります。
静的メソッド | 詳細 |
---|---|
By.all |
すべての要素を返す |
By.css(selector) |
一致する CSS セレクターを持つ要素を返す |
By.directive(directive) |
ディレクティブクラスのインスタンスに Angular が一致させた要素を返す |
app/hero/hero-list.component.spec.ts
import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {DebugElement} from '@angular/core';import {Router} from '@angular/router';import {addMatchers} from '../../testing';import {HeroService} from '../model/hero.service';import {getTestHeroes, TestHeroService} from '../model/testing/test-hero.service';import {HeroListComponent} from './hero-list.component';import {HighlightDirective} from '../shared/highlight.directive';import {appConfig} from '../app.config';const HEROES = getTestHeroes();let comp: HeroListComponent;let fixture: ComponentFixture<HeroListComponent>;let page: Page;/////// Tests //////describe('HeroListComponent', () => { beforeEach(waitForAsync(() => { addMatchers(); const routerSpy = jasmine.createSpyObj('Router', ['navigate']); TestBed.configureTestingModule( Object.assign({}, appConfig, { providers: [ {provide: HeroService, useClass: TestHeroService}, {provide: Router, useValue: routerSpy}, ], }), ) .compileComponents() .then(createComponent); })); it('should display heroes', () => { expect(page.heroRows.length).toBeGreaterThan(0); }); it('1st hero should match 1st test hero', () => { const expectedHero = HEROES[0]; const actualHero = page.heroRows[0].textContent; expect(actualHero).withContext('hero.id').toContain(expectedHero.id.toString()); expect(actualHero).withContext('hero.name').toContain(expectedHero.name); }); it('should select hero on click', fakeAsync(() => { const expectedHero = HEROES[1]; const btn = page.heroRows[1].querySelector('button'); btn!.dispatchEvent(new Event('click')); tick(); // `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService expect(comp.selectedHero).toEqual(expectedHero); })); it('should navigate to selected hero detail on click', fakeAsync(() => { const expectedHero = HEROES[1]; const btn = page.heroRows[1].querySelector('button'); btn!.dispatchEvent(new Event('click')); tick(); // should have navigated expect(page.navSpy.calls.any()).withContext('navigate called').toBe(true); // composed hero detail will be URL like 'heroes/42' // expect link array with the route path and hero id // first argument to router.navigate is link array const navArgs = page.navSpy.calls.first().args[0]; expect(navArgs[0]).withContext('nav to heroes detail URL').toContain('heroes'); expect(navArgs[1]).withContext('expected hero.id').toBe(expectedHero.id); })); it('should find `HighlightDirective` with `By.directive', () => { // 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)); expect(h2).toBe(directive); }); it('should color header with `HighlightDirective`', () => { const h2 = page.highlightDe.nativeElement as HTMLElement; const bgColor = h2.style.backgroundColor; // different browsers report color values differently const isExpectedColor = bgColor === 'gold' || bgColor === 'rgb(255, 215, 0)'; expect(isExpectedColor).withContext('backgroundColor').toBe(true); }); it("the `HighlightDirective` is among the element's providers", () => { expect(page.highlightDe.providerTokens) .withContext('HighlightDirective') .toContain(HighlightDirective); });});/////////// Helpers //////** Create the component and set the `page` test variables */function createComponent() { fixture = TestBed.createComponent(HeroListComponent); comp = fixture.componentInstance; // change detection triggers ngOnInit which gets a hero fixture.detectChanges(); return fixture.whenStable().then(() => { // got the heroes and updated component // change detection updates the view fixture.detectChanges(); page = new Page(); });}class Page { /** Hero line elements */ heroRows: HTMLLIElement[]; /** Highlighted DebugElement */ highlightDe: DebugElement; /** Spy on router navigate method */ navSpy: jasmine.Spy; constructor() { const heroRowNodes = fixture.nativeElement.querySelectorAll('li'); this.heroRows = Array.from(heroRowNodes); // Find the first element with an attached HighlightDirective this.highlightDe = fixture.debugElement.query(By.directive(HighlightDirective)); // Get the component's injected router navigation spy const routerSpy = fixture.debugElement.injector.get(Router); this.navSpy = routerSpy.navigate as jasmine.Spy; }}