サービスが意図通りに動作していることを確認するには、サービス専用のテストを作成できます。
サービスは、多くの場合、ユニットテストを実行するのに最もスムーズなファイルです。
以下は、Angularテストユーティリティの助けを借りずに記述された ValueService
の同期および非同期のユニットテストです。
app/demo/demo.spec.ts
import {LightswitchComponent, MasterService, ValueService, ReversePipe} from './demo';///////// Fakes /////////export class FakeValueService extends ValueService { override value = 'faked service value';}////////////////////////describe('demo (no TestBed):', () => { // Straight Jasmine testing without Angular's testing support describe('ValueService', () => { let service: ValueService; beforeEach(() => { service = new ValueService(); }); it('#getValue should return real value', () => { expect(service.getValue()).toBe('real value'); }); it('#getObservableValue should return value from observable', (done: DoneFn) => { service.getObservableValue().subscribe((value) => { expect(value).toBe('observable value'); done(); }); }); it('#getPromiseValue should return value from a promise', (done: DoneFn) => { service.getPromiseValue().then((value) => { expect(value).toBe('promise value'); done(); }); }); }); // MasterService requires injection of a ValueService describe('MasterService without Angular testing support', () => { let masterService: MasterService; it('#getValue should return real value from the real service', () => { masterService = new MasterService(new ValueService()); expect(masterService.getValue()).toBe('real value'); }); it('#getValue should return faked value from a fakeService', () => { masterService = new MasterService(new FakeValueService()); expect(masterService.getValue()).toBe('faked service value'); }); it('#getValue should return faked value from a fake object', () => { const fake = {getValue: () => 'fake value'}; masterService = new MasterService(fake as ValueService); expect(masterService.getValue()).toBe('fake value'); }); it('#getValue should return stubbed value from a spy', () => { // create `getValue` spy on an object representing the ValueService const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValue']); // set the value to return when the `getValue` spy is called. const stubValue = 'stub value'; valueServiceSpy.getValue.and.returnValue(stubValue); masterService = new MasterService(valueServiceSpy); 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('MasterService (no beforeEach)', () => { it('#getValue should return stubbed value from a spy', () => { const {masterService, stubValue, valueServiceSpy} = setup(); 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); }); 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}; } }); describe('ReversePipe', () => { let pipe: ReversePipe; beforeEach(() => { pipe = new ReversePipe(); }); it('transforms "abc" to "cba"', () => { expect(pipe.transform('abc')).toBe('cba'); }); it('no change to palindrome: "able was I ere I saw elba"', () => { const palindrome = 'able was I ere I saw elba'; expect(pipe.transform(palindrome)).toBe(palindrome); }); }); describe('LightswitchComp', () => { it('#clicked() should toggle #isOn', () => { const comp = new LightswitchComponent(); expect(comp.isOn).withContext('off at first').toBe(false); comp.clicked(); expect(comp.isOn).withContext('on after click').toBe(true); comp.clicked(); expect(comp.isOn).withContext('off after second click').toBe(false); }); it('#clicked() should set #message to "is on"', () => { const comp = new LightswitchComponent(); expect(comp.message) .withContext('off at first') .toMatch(/is off/i); comp.clicked(); expect(comp.message).withContext('on after clicked').toMatch(/is on/i); }); });});
依存関係のあるサービス
サービスは、Angular がコンストラクターに注入する他のサービスに依存することがよくあります。 多くの場合、サービスのコンストラクターを呼び出す際に、これらの依存関係を手動で作成して 注入 できます。
MasterService
は、単純な例です。
app/demo/demo.ts
import { Component, ContentChildren, Directive, EventEmitter, HostBinding, HostListener, Injectable, Input, OnChanges, OnDestroy, OnInit, Optional, Output, Pipe, PipeTransform, SimpleChanges,} from '@angular/core';import {FormsModule} from '@angular/forms';import {of} from 'rxjs';import {delay} from 'rxjs/operators';import {sharedImports} from '../shared/shared';////////// The App: Services and Components for the tests. //////////////export interface Hero { name: string;}////////// Services ///////////////@Injectable()export class ValueService { value = 'real value'; getValue() { return this.value; } setValue(value: string) { this.value = value; } getObservableValue() { return of('observable value'); } getPromiseValue() { return Promise.resolve('promise value'); } getObservableDelayValue() { return of('observable delay value').pipe(delay(10)); }}@Injectable()export class MasterService { constructor(private valueService: ValueService) {} getValue() { return this.valueService.getValue(); }}/////////// Pipe /////////////////* * Reverse the input string. */@Pipe({name: 'reverse', standalone: true})export class ReversePipe implements PipeTransform { transform(s: string) { let r = ''; for (let i = s.length; i; ) { r += s[--i]; } return r; }}//////////// Components /////////////@Component({ standalone: true, selector: 'bank-account', template: ` Bank Name: {{ bank }} Account Id: {{ id }} `,})export class BankAccountComponent { @Input() bank = ''; @Input('account') id = ''; // Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future? // constructor(private renderer: Renderer, private el: ElementRef ) { // renderer.setElementProperty(el.nativeElement, 'customProperty', true); // }}/** A component with attributes, styles, classes, and property setting */@Component({ standalone: true, selector: 'bank-account-parent', template: ` <bank-account bank="RBC" account="4747" [style.width.px]="width" [style.color]="color" [class.closed]="isClosed" [class.open]="!isClosed" > </bank-account> `, imports: [BankAccountComponent],})export class BankAccountParentComponent { width = 200; color = 'red'; isClosed = true;}@Component({ standalone: true, selector: 'lightswitch-comp', template: ` <button type="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'}`; }}@Component({ standalone: true, selector: 'child-1', template: '<span>Child-1({{text}})</span>',})export class Child1Component { @Input() text = 'Original';}@Component({ standalone: true, selector: 'child-2', template: '<div>Child-2({{text}})</div>',})export class Child2Component { @Input() text = '';}@Component({ standalone: true, selector: 'child-3', template: '<div>Child-3({{text}})</div>',})export class Child3Component { @Input() text = '';}@Component({ standalone: true, selector: 'input-comp', template: '<input [(ngModel)]="name">', imports: [FormsModule],})export class InputComponent { name = 'John';}/* Prefer this metadata syntax */// @Directive({// selector: 'input[value]',// host: {// '[value]': 'value',// '(input)': 'valueChange.emit($event.target.value)'// },// inputs: ['value'],// outputs: ['valueChange']// })// export class InputValueBinderDirective {// value: any;// valueChange: EventEmitter<any> = new EventEmitter();// }// As the styleguide recommends@Directive({standalone: true, selector: 'input[value]'})export class InputValueBinderDirective { @HostBinding() @Input() value: any; @Output() valueChange: EventEmitter<any> = new EventEmitter(); @HostListener('input', ['$event.target.value']) onInput(value: any) { this.valueChange.emit(value); }}@Component({ standalone: true, selector: 'input-value-comp', template: ` Name: <input [value]="name" /> {{ name }} `,})export class InputValueBinderComponent { name = 'Sally'; // initial value}@Component({ standalone: true, selector: 'parent-comp', imports: [Child1Component], template: 'Parent(<child-1></child-1>)',})export class ParentComponent {}@Component({ standalone: true, selector: 'io-comp', template: '<button type="button" class="hero" (click)="click()">Original {{hero.name}}</button>',})export class IoComponent { @Input() hero!: Hero; @Output() selected = new EventEmitter<Hero>(); click() { this.selected.emit(this.hero); }}@Component({ standalone: true, selector: 'io-parent-comp', template: ` @if (!selectedHero) { <p><i>Click to select a hero</i></p> } @if (selectedHero) { <p>The selected hero is {{ selectedHero.name }}</p> } @for (hero of heroes; track hero) { <io-comp [hero]="hero" (selected)="onSelect($event)"> </io-comp> } `, imports: [IoComponent, sharedImports],})export class IoParentComponent { heroes: Hero[] = [{name: 'Bob'}, {name: 'Carol'}, {name: 'Ted'}, {name: 'Alice'}]; selectedHero!: Hero; onSelect(hero: Hero) { this.selectedHero = hero; }}@Component({ standalone: true, selector: 'my-if-comp', template: 'MyIf(@if (showMore) {<span>More</span>})', imports: [sharedImports],})export class MyIfComponent { showMore = false;}@Component({ standalone: true, selector: 'my-service-comp', template: 'injected value: {{valueService.value}}', providers: [ValueService],})export class TestProvidersComponent { constructor(public valueService: ValueService) {}}@Component({ standalone: true, selector: 'my-service-comp', template: 'injected value: {{valueService.value}}', viewProviders: [ValueService],})export class TestViewProvidersComponent { constructor(public valueService: ValueService) {}}@Component({ standalone: true, selector: 'external-template-comp', templateUrl: './demo-external-template.html',})export class ExternalTemplateComponent implements OnInit { serviceValue = ''; constructor(@Optional() private service?: ValueService) {} ngOnInit() { if (this.service) { this.serviceValue = this.service.getValue(); } }}@Component({ standalone: true, selector: 'comp-w-ext-comp', imports: [ExternalTemplateComponent], template: ` <h3>comp-w-ext-comp</h3> <external-template-comp></external-template-comp> `,})export class InnerCompWithExternalTemplateComponent {}@Component({standalone: true, selector: 'needs-content', template: '<ng-content></ng-content>'})export class NeedsContentComponent { // children with #content local variable @ContentChildren('content') children: any;}///////// MyIfChildComp ////////@Component({ standalone: true, selector: 'my-if-child-1', template: ` <h4>MyIfChildComp</h4> <div> <label for="child-value" >Child value: <input id="child-value" [(ngModel)]="childValue" /> </label> </div> <p><i>Change log:</i></p> @for (log of changeLog; track log; let i = $index) { <div>{{ i + 1 }} - {{ log }}</div> }`, imports: [FormsModule, sharedImports],})export class MyIfChildComponent implements OnInit, OnChanges, OnDestroy { @Input() value = ''; @Output() valueChange = new EventEmitter<string>(); get childValue() { return this.value; } set childValue(v: string) { if (this.value === v) { return; } this.value = v; this.valueChange.emit(v); } changeLog: string[] = []; ngOnInitCalled = false; ngOnChangesCounter = 0; ngOnDestroyCalled = false; ngOnInit() { this.ngOnInitCalled = true; this.changeLog.push('ngOnInit called'); } ngOnDestroy() { this.ngOnDestroyCalled = true; this.changeLog.push('ngOnDestroy called'); } ngOnChanges(changes: SimpleChanges) { for (const propName in changes) { this.ngOnChangesCounter += 1; const prop = changes[propName]; const cur = JSON.stringify(prop.currentValue); const prev = JSON.stringify(prop.previousValue); this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`); } }}///////// MyIfParentComp ////////@Component({ standalone: true, selector: 'my-if-parent-comp', template: ` <h3>MyIfParentComp</h3> <label for="parent" >Parent value: <input id="parent" [(ngModel)]="parentValue" /> </label> <button type="button" (click)="clicked()">{{ toggleLabel }} Child</button><br /> @if (showChild) { <div style="margin: 4px; padding: 4px; background-color: aliceblue;"> <my-if-child-1 [(value)]="parentValue"></my-if-child-1> </div> } `, imports: [FormsModule, MyIfChildComponent, sharedImports],})export class MyIfParentComponent implements OnInit { ngOnInitCalled = false; parentValue = 'Hello, World'; showChild = false; toggleLabel = 'Unknown'; ngOnInit() { this.ngOnInitCalled = true; this.clicked(); } clicked() { this.showChild = !this.showChild; this.toggleLabel = this.showChild ? 'Close' : 'Show'; }}@Component({ standalone: true, selector: 'reverse-pipe-comp', template: ` <input [(ngModel)]="text" /> <span>{{ text | reverse }}</span> `, imports: [ReversePipe, FormsModule],})export class ReversePipeComponent { text = 'my dog has fleas.';}@Component({ standalone: true, imports: [NeedsContentComponent], template: '<div>Replace Me</div>',})export class ShellComponent {}@Component({ standalone: true, selector: 'demo-comp', template: ` <h1>Specs Demo</h1> <my-if-parent-comp></my-if-parent-comp> <hr /> <h3>Input/Output Component</h3> <io-parent-comp></io-parent-comp> <hr /> <h3>External Template Component</h3> <external-template-comp></external-template-comp> <hr /> <h3>Component With External Template Component</h3> <comp-w-ext-comp></comp-w-ext-comp> <hr /> <h3>Reverse Pipe</h3> <reverse-pipe-comp></reverse-pipe-comp> <hr /> <h3>InputValueBinder Directive</h3> <input-value-comp></input-value-comp> <hr /> <h3>Button Component</h3> <lightswitch-comp></lightswitch-comp> <hr /> <h3>Needs Content</h3> <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> `, imports: [ Child1Component, Child2Component, Child3Component, ExternalTemplateComponent, InnerCompWithExternalTemplateComponent, InputValueBinderComponent, IoParentComponent, LightswitchComponent, NeedsContentComponent, ReversePipeComponent, MyIfParentComponent, ],})export class DemoComponent {}//////// Aggregations ////////////export const demoProviders = [MasterService, ValueService];
MasterService
は、唯一のメソッドである getValue
を、注入された ValueService
に委譲します。
テストを行うには、いくつかの方法があります。
app/demo/demo.spec.ts
import {LightswitchComponent, MasterService, ValueService, ReversePipe} from './demo';///////// Fakes /////////export class FakeValueService extends ValueService { override value = 'faked service value';}////////////////////////describe('demo (no TestBed):', () => { // Straight Jasmine testing without Angular's testing support describe('ValueService', () => { let service: ValueService; beforeEach(() => { service = new ValueService(); }); it('#getValue should return real value', () => { expect(service.getValue()).toBe('real value'); }); it('#getObservableValue should return value from observable', (done: DoneFn) => { service.getObservableValue().subscribe((value) => { expect(value).toBe('observable value'); done(); }); }); it('#getPromiseValue should return value from a promise', (done: DoneFn) => { service.getPromiseValue().then((value) => { expect(value).toBe('promise value'); done(); }); }); }); // MasterService requires injection of a ValueService describe('MasterService without Angular testing support', () => { let masterService: MasterService; it('#getValue should return real value from the real service', () => { masterService = new MasterService(new ValueService()); expect(masterService.getValue()).toBe('real value'); }); it('#getValue should return faked value from a fakeService', () => { masterService = new MasterService(new FakeValueService()); expect(masterService.getValue()).toBe('faked service value'); }); it('#getValue should return faked value from a fake object', () => { const fake = {getValue: () => 'fake value'}; masterService = new MasterService(fake as ValueService); expect(masterService.getValue()).toBe('fake value'); }); it('#getValue should return stubbed value from a spy', () => { // create `getValue` spy on an object representing the ValueService const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValue']); // set the value to return when the `getValue` spy is called. const stubValue = 'stub value'; valueServiceSpy.getValue.and.returnValue(stubValue); masterService = new MasterService(valueServiceSpy); 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('MasterService (no beforeEach)', () => { it('#getValue should return stubbed value from a spy', () => { const {masterService, stubValue, valueServiceSpy} = setup(); 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); }); 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}; } }); describe('ReversePipe', () => { let pipe: ReversePipe; beforeEach(() => { pipe = new ReversePipe(); }); it('transforms "abc" to "cba"', () => { expect(pipe.transform('abc')).toBe('cba'); }); it('no change to palindrome: "able was I ere I saw elba"', () => { const palindrome = 'able was I ere I saw elba'; expect(pipe.transform(palindrome)).toBe(palindrome); }); }); describe('LightswitchComp', () => { it('#clicked() should toggle #isOn', () => { const comp = new LightswitchComponent(); expect(comp.isOn).withContext('off at first').toBe(false); comp.clicked(); expect(comp.isOn).withContext('on after click').toBe(true); comp.clicked(); expect(comp.isOn).withContext('off after second click').toBe(false); }); it('#clicked() should set #message to "is on"', () => { const comp = new LightswitchComponent(); expect(comp.message) .withContext('off at first') .toMatch(/is off/i); comp.clicked(); expect(comp.message).withContext('on after clicked').toMatch(/is on/i); }); });});
最初のテストでは、new
で ValueService
を作成し、MasterService
コンストラクターに渡します。
ただし、実際のサービスを注入することは、ほとんどの場合、うまく機能しません。ほとんどの依存サービスは作成と制御が難しいからです。
代わりに、依存関係をモック化し、ダミー値を使用するか、関連するサービスメソッドに スパイ を作成します。
HELPFUL: スパイは、通常、サービスをモック化する最良の方法なので、スパイを使用することをお勧めします。
これらの標準的なテストテクニックは、サービスを単体でユニットテストするのに適しています。
ただし、ほとんどの場合、Angular の依存関係注入を使用してサービスをアプリケーションクラスに注入します。そのため、その使用パターンを反映したテストを行う必要があります。 Angular のテストユーティリティを使用すると、注入されたサービスがどのように動作するかを簡単に調査できます。
TestBed
を使用したサービスのテスト
アプリケーションは、Angular の 依存関係注入 (DI) に依存してサービスを作成します。 サービスが依存サービスを持っている場合、DI はその依存サービスを見つけたり、作成します。 さらに、その依存サービスに独自の依存関係がある場合、DI はそれらも探し出して作成します。
サービスの 消費者 として、あなたはこれらについて心配する必要はありません。 コンストラクター引数の順序や、それらがどのように作成されるかについて心配する必要はありません。
サービスの テスター として、少なくともサービス依存関係の最初のレベルについて考える必要はありますが、TestBed
テストユーティリティを使用してサービスを提供して作成し、コンストラクター引数の順序を処理するときは、Angular DI にサービスの作成を任せることができます。
Angular TestBed
TestBed
は、Angular のテストユーティリティの中で最も重要なものです。
TestBed
は、Angular の @NgModule をエミュレートする、動的に構築された Angular の テスト モジュールを作成します。
TestBed.configureTestingModule()
メソッドは、@NgModule のほとんどのプロパティを持つことができるメタデータオブジェクトを受け取ります。
サービスをテストするには、テストまたはモックするサービスの配列を providers
メタデータプロパティに設定します。
app/demo/demo.testbed.spec.ts (beforeEach で ValueService を提供)
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({ standalone: true, 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({ standalone: true, selector: 'child-1', template: 'Fake Child',})class FakeChildComponent {}@Component({ standalone: true, selector: 'grandchild-1', template: 'Fake Grandchild',})class FakeGrandchildComponent {}@Component({ standalone: true, selector: 'child-1', imports: [FakeGrandchildComponent], template: 'Fake Child(<grandchild-1></grandchild-1>)',})class FakeChildWithGrandchildComponent {}@Injectable()class FakeValueService extends ValueService { override value = 'faked value';}
次に、サービスクラスを引数として TestBed.inject()
を呼び出して、テスト内でサービスを注入します。
HELPFUL: TestBed.get()
は、Angular バージョン 9 以降で非推奨になりました。
重大な変更を最小限に抑えるため、Angular は TestBed.inject()
という新しい関数を導入しました。これは、代わりに使用する必要があります。
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({ standalone: true, 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({ standalone: true, selector: 'child-1', template: 'Fake Child',})class FakeChildComponent {}@Component({ standalone: true, selector: 'grandchild-1', template: 'Fake Grandchild',})class FakeGrandchildComponent {}@Component({ standalone: true, selector: 'child-1', imports: [FakeGrandchildComponent], template: 'Fake Child(<grandchild-1></grandchild-1>)',})class FakeChildWithGrandchildComponent {}@Injectable()class FakeValueService extends ValueService { override value = 'faked value';}
または、セットアップの一部としてサービスを注入したい場合は、beforeEach()
内で行います。
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({ standalone: true, 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({ standalone: true, selector: 'child-1', template: 'Fake Child',})class FakeChildComponent {}@Component({ standalone: true, selector: 'grandchild-1', template: 'Fake Grandchild',})class FakeGrandchildComponent {}@Component({ standalone: true, selector: 'child-1', imports: [FakeGrandchildComponent], template: 'Fake Child(<grandchild-1></grandchild-1>)',})class FakeChildWithGrandchildComponent {}@Injectable()class FakeValueService extends ValueService { override value = 'faked value';}
依存関係のあるサービスをテストする場合は、providers
配列にモックを提供します。
次の例では、モックはスパイオブジェクトです。
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({ standalone: true, 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({ standalone: true, selector: 'child-1', template: 'Fake Child',})class FakeChildComponent {}@Component({ standalone: true, selector: 'grandchild-1', template: 'Fake Grandchild',})class FakeGrandchildComponent {}@Component({ standalone: true, selector: 'child-1', imports: [FakeGrandchildComponent], template: 'Fake Child(<grandchild-1></grandchild-1>)',})class FakeChildWithGrandchildComponent {}@Injectable()class FakeValueService extends ValueService { override value = 'faked value';}
テストでは、以前と同じように、そのスパイを使用します。
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({ standalone: true, 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({ standalone: true, selector: 'child-1', template: 'Fake Child',})class FakeChildComponent {}@Component({ standalone: true, selector: 'grandchild-1', template: 'Fake Grandchild',})class FakeGrandchildComponent {}@Component({ standalone: true, selector: 'child-1', imports: [FakeGrandchildComponent], template: 'Fake Child(<grandchild-1></grandchild-1>)',})class FakeChildWithGrandchildComponent {}@Injectable()class FakeValueService extends ValueService { override value = 'faked value';}
beforeEach()
を使用しないテスト
このガイドのほとんどのテストスイートでは、beforeEach()
を呼び出して各 it()
テストの前提条件を設定し、TestBed
にクラスの作成とサービスの注入を任せています。
beforeEach()
を呼び出さない、別のテストの考え方があり、TestBed
を使用せず、クラスを明示的に作成することを好みます。
このスタイルで MasterService
のテストの1つを書き直す方法を以下に示します。
最初に、セットアップ 関数に、再利用可能な準備コードを beforeEach()
の代わりに配置します。
app/demo/demo.spec.ts (setup)
import {LightswitchComponent, MasterService, ValueService, ReversePipe} from './demo';///////// Fakes /////////export class FakeValueService extends ValueService { override value = 'faked service value';}////////////////////////describe('demo (no TestBed):', () => { // Straight Jasmine testing without Angular's testing support describe('ValueService', () => { let service: ValueService; beforeEach(() => { service = new ValueService(); }); it('#getValue should return real value', () => { expect(service.getValue()).toBe('real value'); }); it('#getObservableValue should return value from observable', (done: DoneFn) => { service.getObservableValue().subscribe((value) => { expect(value).toBe('observable value'); done(); }); }); it('#getPromiseValue should return value from a promise', (done: DoneFn) => { service.getPromiseValue().then((value) => { expect(value).toBe('promise value'); done(); }); }); }); // MasterService requires injection of a ValueService describe('MasterService without Angular testing support', () => { let masterService: MasterService; it('#getValue should return real value from the real service', () => { masterService = new MasterService(new ValueService()); expect(masterService.getValue()).toBe('real value'); }); it('#getValue should return faked value from a fakeService', () => { masterService = new MasterService(new FakeValueService()); expect(masterService.getValue()).toBe('faked service value'); }); it('#getValue should return faked value from a fake object', () => { const fake = {getValue: () => 'fake value'}; masterService = new MasterService(fake as ValueService); expect(masterService.getValue()).toBe('fake value'); }); it('#getValue should return stubbed value from a spy', () => { // create `getValue` spy on an object representing the ValueService const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValue']); // set the value to return when the `getValue` spy is called. const stubValue = 'stub value'; valueServiceSpy.getValue.and.returnValue(stubValue); masterService = new MasterService(valueServiceSpy); 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('MasterService (no beforeEach)', () => { it('#getValue should return stubbed value from a spy', () => { const {masterService, stubValue, valueServiceSpy} = setup(); 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); }); 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}; } }); describe('ReversePipe', () => { let pipe: ReversePipe; beforeEach(() => { pipe = new ReversePipe(); }); it('transforms "abc" to "cba"', () => { expect(pipe.transform('abc')).toBe('cba'); }); it('no change to palindrome: "able was I ere I saw elba"', () => { const palindrome = 'able was I ere I saw elba'; expect(pipe.transform(palindrome)).toBe(palindrome); }); }); describe('LightswitchComp', () => { it('#clicked() should toggle #isOn', () => { const comp = new LightswitchComponent(); expect(comp.isOn).withContext('off at first').toBe(false); comp.clicked(); expect(comp.isOn).withContext('on after click').toBe(true); comp.clicked(); expect(comp.isOn).withContext('off after second click').toBe(false); }); it('#clicked() should set #message to "is on"', () => { const comp = new LightswitchComponent(); expect(comp.message) .withContext('off at first') .toMatch(/is off/i); comp.clicked(); expect(comp.message).withContext('on after clicked').toMatch(/is on/i); }); });});
setup()
関数は、テストで参照できる可能性のある変数 masterService
などの変数を、オブジェクトリテラルとして返します。
describe()
の本文には、半グローバル 変数(例:let masterService: MasterService
)は定義しません。
次に、各テストは、テスト対象の操作や期待の主張を続行する前に、最初の行で setup()
を呼び出します。
import {LightswitchComponent, MasterService, ValueService, ReversePipe} from './demo';///////// Fakes /////////export class FakeValueService extends ValueService { override value = 'faked service value';}////////////////////////describe('demo (no TestBed):', () => { // Straight Jasmine testing without Angular's testing support describe('ValueService', () => { let service: ValueService; beforeEach(() => { service = new ValueService(); }); it('#getValue should return real value', () => { expect(service.getValue()).toBe('real value'); }); it('#getObservableValue should return value from observable', (done: DoneFn) => { service.getObservableValue().subscribe((value) => { expect(value).toBe('observable value'); done(); }); }); it('#getPromiseValue should return value from a promise', (done: DoneFn) => { service.getPromiseValue().then((value) => { expect(value).toBe('promise value'); done(); }); }); }); // MasterService requires injection of a ValueService describe('MasterService without Angular testing support', () => { let masterService: MasterService; it('#getValue should return real value from the real service', () => { masterService = new MasterService(new ValueService()); expect(masterService.getValue()).toBe('real value'); }); it('#getValue should return faked value from a fakeService', () => { masterService = new MasterService(new FakeValueService()); expect(masterService.getValue()).toBe('faked service value'); }); it('#getValue should return faked value from a fake object', () => { const fake = {getValue: () => 'fake value'}; masterService = new MasterService(fake as ValueService); expect(masterService.getValue()).toBe('fake value'); }); it('#getValue should return stubbed value from a spy', () => { // create `getValue` spy on an object representing the ValueService const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValue']); // set the value to return when the `getValue` spy is called. const stubValue = 'stub value'; valueServiceSpy.getValue.and.returnValue(stubValue); masterService = new MasterService(valueServiceSpy); 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('MasterService (no beforeEach)', () => { it('#getValue should return stubbed value from a spy', () => { const {masterService, stubValue, valueServiceSpy} = setup(); 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); }); 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}; } }); describe('ReversePipe', () => { let pipe: ReversePipe; beforeEach(() => { pipe = new ReversePipe(); }); it('transforms "abc" to "cba"', () => { expect(pipe.transform('abc')).toBe('cba'); }); it('no change to palindrome: "able was I ere I saw elba"', () => { const palindrome = 'able was I ere I saw elba'; expect(pipe.transform(palindrome)).toBe(palindrome); }); }); describe('LightswitchComp', () => { it('#clicked() should toggle #isOn', () => { const comp = new LightswitchComponent(); expect(comp.isOn).withContext('off at first').toBe(false); comp.clicked(); expect(comp.isOn).withContext('on after click').toBe(true); comp.clicked(); expect(comp.isOn).withContext('off after second click').toBe(false); }); it('#clicked() should set #message to "is on"', () => { const comp = new LightswitchComponent(); expect(comp.message) .withContext('off at first') .toMatch(/is off/i); comp.clicked(); expect(comp.message).withContext('on after clicked').toMatch(/is on/i); }); });});
テストで デストラクチャリング代入 を使用して、必要なセットアップ変数を抽出したことに注意してください。
import {LightswitchComponent, MasterService, ValueService, ReversePipe} from './demo';///////// Fakes /////////export class FakeValueService extends ValueService { override value = 'faked service value';}////////////////////////describe('demo (no TestBed):', () => { // Straight Jasmine testing without Angular's testing support describe('ValueService', () => { let service: ValueService; beforeEach(() => { service = new ValueService(); }); it('#getValue should return real value', () => { expect(service.getValue()).toBe('real value'); }); it('#getObservableValue should return value from observable', (done: DoneFn) => { service.getObservableValue().subscribe((value) => { expect(value).toBe('observable value'); done(); }); }); it('#getPromiseValue should return value from a promise', (done: DoneFn) => { service.getPromiseValue().then((value) => { expect(value).toBe('promise value'); done(); }); }); }); // MasterService requires injection of a ValueService describe('MasterService without Angular testing support', () => { let masterService: MasterService; it('#getValue should return real value from the real service', () => { masterService = new MasterService(new ValueService()); expect(masterService.getValue()).toBe('real value'); }); it('#getValue should return faked value from a fakeService', () => { masterService = new MasterService(new FakeValueService()); expect(masterService.getValue()).toBe('faked service value'); }); it('#getValue should return faked value from a fake object', () => { const fake = {getValue: () => 'fake value'}; masterService = new MasterService(fake as ValueService); expect(masterService.getValue()).toBe('fake value'); }); it('#getValue should return stubbed value from a spy', () => { // create `getValue` spy on an object representing the ValueService const valueServiceSpy = jasmine.createSpyObj('ValueService', ['getValue']); // set the value to return when the `getValue` spy is called. const stubValue = 'stub value'; valueServiceSpy.getValue.and.returnValue(stubValue); masterService = new MasterService(valueServiceSpy); 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('MasterService (no beforeEach)', () => { it('#getValue should return stubbed value from a spy', () => { const {masterService, stubValue, valueServiceSpy} = setup(); 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); }); 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}; } }); describe('ReversePipe', () => { let pipe: ReversePipe; beforeEach(() => { pipe = new ReversePipe(); }); it('transforms "abc" to "cba"', () => { expect(pipe.transform('abc')).toBe('cba'); }); it('no change to palindrome: "able was I ere I saw elba"', () => { const palindrome = 'able was I ere I saw elba'; expect(pipe.transform(palindrome)).toBe(palindrome); }); }); describe('LightswitchComp', () => { it('#clicked() should toggle #isOn', () => { const comp = new LightswitchComponent(); expect(comp.isOn).withContext('off at first').toBe(false); comp.clicked(); expect(comp.isOn).withContext('on after click').toBe(true); comp.clicked(); expect(comp.isOn).withContext('off after second click').toBe(false); }); it('#clicked() should set #message to "is on"', () => { const comp = new LightswitchComponent(); expect(comp.message) .withContext('off at first') .toMatch(/is off/i); comp.clicked(); expect(comp.message).withContext('on after clicked').toMatch(/is on/i); }); });});
多くの開発者は、このアプローチは従来の beforeEach()
スタイルよりもクリーンで明示的だと感じるでしょう。
このテストガイドでは、従来のスタイルと、デフォルトの CLI スキーマ で beforeEach()
と TestBed
を使用してテストファイルが生成されますが、独自のプロジェクトで この代替アプローチ を採用することは自由です。
HTTP サービスのテスト
リモートサーバーにHTTP呼び出しするデータサービスは、通常、Angularの HttpClient
サービスを注入して委譲し、XHRを呼び出します。
依存関係が注入された HttpClient
スパイを使用して、データサービスをテストできます。
app/model/hero.service.spec.ts (スパイを使用したテスト)
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';// Other importsimport {TestBed} from '@angular/core/testing';import {HttpClient, HttpResponse, HttpErrorResponse} from '@angular/common/http';import {asyncData, asyncError} from '../../testing/async-observable-helpers';import {Hero} from './hero';import {HeroService} from './hero.service';describe('HeroesService (with spies)', () => { let httpClientSpy: jasmine.SpyObj<HttpClient>; let heroService: HeroService; beforeEach(() => { // TODO: spy on other methods too httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); heroService = new HeroService(httpClientSpy); }); it('should return expected heroes (HttpClient called once)', (done: DoneFn) => { const expectedHeroes: Hero[] = [ {id: 1, name: 'A'}, {id: 2, name: 'B'}, ]; httpClientSpy.get.and.returnValue(asyncData(expectedHeroes)); heroService.getHeroes().subscribe({ next: (heroes) => { expect(heroes).withContext('expected heroes').toEqual(expectedHeroes); done(); }, error: done.fail, }); expect(httpClientSpy.get.calls.count()).withContext('one call').toBe(1); }); it('should return an error when the server returns a 404', (done: DoneFn) => { const errorResponse = new HttpErrorResponse({ error: 'test 404 error', status: 404, statusText: 'Not Found', }); httpClientSpy.get.and.returnValue(asyncError(errorResponse)); heroService.getHeroes().subscribe({ next: (heroes) => done.fail('expected an error, not heroes'), error: (error) => { expect(error.message).toContain('test 404 error'); done(); }, }); });});describe('HeroesService (with mocks)', () => { let httpClient: HttpClient; let httpTestingController: HttpTestingController; let heroService: HeroService; beforeEach(() => { TestBed.configureTestingModule({ // Import the HttpClient mocking services imports: [HttpClientTestingModule], // Provide the service-under-test providers: [HeroService], }); // Inject the http, test controller, and service-under-test // as they will be referenced by each test. httpClient = TestBed.inject(HttpClient); httpTestingController = TestBed.inject(HttpTestingController); heroService = TestBed.inject(HeroService); }); afterEach(() => { // After every test, assert that there are no more pending requests. httpTestingController.verify(); }); /// HeroService method tests begin /// describe('#getHeroes', () => { let expectedHeroes: Hero[]; beforeEach(() => { heroService = TestBed.inject(HeroService); expectedHeroes = [ {id: 1, name: 'A'}, {id: 2, name: 'B'}, ] as Hero[]; }); it('should return expected heroes (called once)', () => { heroService.getHeroes().subscribe({ next: (heroes) => expect(heroes).withContext('should return expected heroes').toEqual(expectedHeroes), error: fail, }); // HeroService should have made one request to GET heroes from expected URL const req = httpTestingController.expectOne(heroService.heroesUrl); expect(req.request.method).toEqual('GET'); // Respond with the mock heroes req.flush(expectedHeroes); }); it('should be OK returning no heroes', () => { heroService.getHeroes().subscribe({ next: (heroes) => expect(heroes.length).withContext('should have empty heroes array').toEqual(0), error: fail, }); const req = httpTestingController.expectOne(heroService.heroesUrl); req.flush([]); // Respond with no heroes }); it('should turn 404 into a user-friendly error', () => { const msg = 'Deliberate 404'; heroService.getHeroes().subscribe({ next: (heroes) => fail('expected to fail'), error: (error) => expect(error.message).toContain(msg), }); const req = httpTestingController.expectOne(heroService.heroesUrl); // respond with a 404 and the error message in the body req.flush(msg, {status: 404, statusText: 'Not Found'}); }); it('should return expected heroes (called multiple times)', () => { heroService.getHeroes().subscribe(); heroService.getHeroes().subscribe(); heroService.getHeroes().subscribe({ next: (heroes) => expect(heroes).withContext('should return expected heroes').toEqual(expectedHeroes), error: fail, }); const requests = httpTestingController.match(heroService.heroesUrl); expect(requests.length).withContext('calls to getHeroes()').toEqual(3); // Respond to each request with different mock hero results requests[0].flush([]); requests[1].flush([{id: 1, name: 'bob'}]); requests[2].flush(expectedHeroes); }); }); describe('#updateHero', () => { // Expecting the query form of URL so should not 404 when id not found const makeUrl = (id: number) => `${heroService.heroesUrl}/?id=${id}`; it('should update a hero and return it', () => { const updateHero: Hero = {id: 1, name: 'A'}; heroService.updateHero(updateHero).subscribe({ next: (data) => expect(data).withContext('should return the hero').toEqual(updateHero), error: fail, }); // HeroService should have made one request to PUT hero const req = httpTestingController.expectOne(heroService.heroesUrl); expect(req.request.method).toEqual('PUT'); expect(req.request.body).toEqual(updateHero); // Expect server to return the hero after PUT const expectedResponse = new HttpResponse({ status: 200, statusText: 'OK', body: updateHero, }); req.event(expectedResponse); }); it('should turn 404 error into user-facing error', () => { const msg = 'Deliberate 404'; const updateHero: Hero = {id: 1, name: 'A'}; heroService.updateHero(updateHero).subscribe({ next: (heroes) => fail('expected to fail'), error: (error) => expect(error.message).toContain(msg), }); const req = httpTestingController.expectOne(heroService.heroesUrl); // respond with a 404 and the error message in the body req.flush(msg, {status: 404, statusText: 'Not Found'}); }); it('should turn network error into user-facing error', (done) => { // Create mock ProgressEvent with type `error`, raised when something goes wrong at // the network level. Connection timeout, DNS error, offline, etc. const errorEvent = new ProgressEvent('error'); const updateHero: Hero = {id: 1, name: 'A'}; heroService.updateHero(updateHero).subscribe({ next: (heroes) => fail('expected to fail'), error: (error) => { expect(error).toBe(errorEvent); done(); }, }); const req = httpTestingController.expectOne(heroService.heroesUrl); // Respond with mock error req.error(errorEvent); }); }); // TODO: test other HeroService methods});
IMPORTANT: HeroService
メソッドは Observable
を返します。
Observableに 登録 することで、(a) 実行させ、(b) メソッドが成功したか失敗したかをアサートする必要があります。
subscribe()
メソッドは、成功 (next
) と失敗 (error
) のコールバックを受け取ります。
エラーを捕捉するために、両方の コールバックを提供してください。
これを怠ると、非同期でキャッチされないObservableエラーが発生し、テストランナーは別のテストによるエラーであると判断する可能性があります。
HttpClientTestingModule
データサービスと HttpClient
の間の拡張されたやり取りは複雑で、スパイでモック化するのは難しい場合があります。
HttpClientTestingModule
を使用すると、これらのテストシナリオをより管理しやすくなります。
このガイドに付属する コードサンプル では HttpClientTestingModule
が示されていますが、このページでは、HttpClientTestingModule
を使用したテストを詳しく説明している HTTP ガイド を参照します。