Guias Detalhados
Testing

Cenários de testes de components

Este guia explora casos de uso comuns de testes de components.

Binding de component

Na aplicação de exemplo, o BannerComponent apresenta texto de título estático no template HTML.

Após algumas mudanças, o BannerComponent apresenta um título dinâmico fazendo binding com a propriedade title do component assim.

app/banner/banner.component.ts

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

Por mais mínimo que seja, você decide adicionar um teste para confirmar que o component realmente exibe o conteúdo correto onde você pensa que deveria.

Query para o <h1>

Você escreverá uma sequência de testes que inspecionam o valor do elemento <h1> que envolve o binding de interpolação da propriedade title.

Você atualiza o beforeEach para encontrar esse elemento com um querySelector HTML padrão e atribuí-lo à variável h1.

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

import {ComponentFixture, TestBed} from '@angular/core/testing';import {BannerComponent} from './banner.component';describe('BannerComponent (inline template)', () => {  let component: BannerComponent;  let fixture: ComponentFixture<BannerComponent>;  let h1: HTMLElement;  beforeEach(() => {    fixture = TestBed.createComponent(BannerComponent);    component = fixture.componentInstance; // BannerComponent test instance    h1 = fixture.nativeElement.querySelector('h1');  });  it('no title in the DOM after createComponent()', () => {    expect(h1.textContent).toEqual('');  });  it('should display original title', () => {    fixture.detectChanges();    expect(h1.textContent).toContain(component.title);  });  it('should display original title after detectChanges()', () => {    fixture.detectChanges();    expect(h1.textContent).toContain(component.title);  });  it('should display a different test title', () => {    component.title = 'Test Title';    fixture.detectChanges();    expect(h1.textContent).toContain('Test Title');  });});

createComponent() não faz bind de dados

Para seu primeiro teste, você gostaria de ver que a tela exibe o title padrão. Seu instinto é escrever um teste que imediatamente inspeciona o <h1> assim:

import {ComponentFixture, TestBed} from '@angular/core/testing';import {BannerComponent} from './banner.component';describe('BannerComponent (inline template)', () => {  let component: BannerComponent;  let fixture: ComponentFixture<BannerComponent>;  let h1: HTMLElement;  beforeEach(() => {    fixture = TestBed.createComponent(BannerComponent);    component = fixture.componentInstance; // BannerComponent test instance    h1 = fixture.nativeElement.querySelector('h1');  });  it('no title in the DOM after createComponent()', () => {    expect(h1.textContent).toEqual('');  });  it('should display original title', () => {    fixture.detectChanges();    expect(h1.textContent).toContain(component.title);  });  it('should display original title after detectChanges()', () => {    fixture.detectChanges();    expect(h1.textContent).toContain(component.title);  });  it('should display a different test title', () => {    component.title = 'Test Title';    fixture.detectChanges();    expect(h1.textContent).toContain('Test Title');  });});

Esse teste falha com a mensagem:

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

Binding acontece quando o Angular executa change detection.

Em produção, change detection é acionado automaticamente quando o Angular cria um component ou o usuário pressiona uma tecla, por exemplo.

O TestBed.createComponent não aciona change detection por padrão; um fato confirmado no teste revisado:

import {ComponentFixture, TestBed} from '@angular/core/testing';import {BannerComponent} from './banner.component';describe('BannerComponent (inline template)', () => {  let component: BannerComponent;  let fixture: ComponentFixture<BannerComponent>;  let h1: HTMLElement;  beforeEach(() => {    fixture = TestBed.createComponent(BannerComponent);    component = fixture.componentInstance; // BannerComponent test instance    h1 = fixture.nativeElement.querySelector('h1');  });  it('no title in the DOM after createComponent()', () => {    expect(h1.textContent).toEqual('');  });  it('should display original title', () => {    fixture.detectChanges();    expect(h1.textContent).toContain(component.title);  });  it('should display original title after detectChanges()', () => {    fixture.detectChanges();    expect(h1.textContent).toContain(component.title);  });  it('should display a different test title', () => {    component.title = 'Test Title';    fixture.detectChanges();    expect(h1.textContent).toContain('Test Title');  });});

detectChanges()

Você pode dizer ao TestBed para executar data binding chamando fixture.detectChanges(). Somente então o <h1> tem o título esperado.

import {ComponentFixture, TestBed} from '@angular/core/testing';import {BannerComponent} from './banner.component';describe('BannerComponent (inline template)', () => {  let component: BannerComponent;  let fixture: ComponentFixture<BannerComponent>;  let h1: HTMLElement;  beforeEach(() => {    fixture = TestBed.createComponent(BannerComponent);    component = fixture.componentInstance; // BannerComponent test instance    h1 = fixture.nativeElement.querySelector('h1');  });  it('no title in the DOM after createComponent()', () => {    expect(h1.textContent).toEqual('');  });  it('should display original title', () => {    fixture.detectChanges();    expect(h1.textContent).toContain(component.title);  });  it('should display original title after detectChanges()', () => {    fixture.detectChanges();    expect(h1.textContent).toContain(component.title);  });  it('should display a different test title', () => {    component.title = 'Test Title';    fixture.detectChanges();    expect(h1.textContent).toContain('Test Title');  });});

Change detection atrasado é intencional e útil. Ele dá ao testador uma oportunidade de inspecionar e alterar o estado do component antes que o Angular inicie data binding e chame lifecycle hooks.

Aqui está outro teste que altera a propriedade title do component antes de chamar fixture.detectChanges().

import {ComponentFixture, TestBed} from '@angular/core/testing';import {BannerComponent} from './banner.component';describe('BannerComponent (inline template)', () => {  let component: BannerComponent;  let fixture: ComponentFixture<BannerComponent>;  let h1: HTMLElement;  beforeEach(() => {    fixture = TestBed.createComponent(BannerComponent);    component = fixture.componentInstance; // BannerComponent test instance    h1 = fixture.nativeElement.querySelector('h1');  });  it('no title in the DOM after createComponent()', () => {    expect(h1.textContent).toEqual('');  });  it('should display original title', () => {    fixture.detectChanges();    expect(h1.textContent).toContain(component.title);  });  it('should display original title after detectChanges()', () => {    fixture.detectChanges();    expect(h1.textContent).toContain(component.title);  });  it('should display a different test title', () => {    component.title = 'Test Title';    fixture.detectChanges();    expect(h1.textContent).toContain('Test Title');  });});

Change detection automático

Os testes de BannerComponent frequentemente chamam detectChanges. Muitos testadores preferem que o ambiente de teste do Angular execute change detection automaticamente como faz em produção.

Isso é possível configurando o TestBed com o provider ComponentFixtureAutoDetect. Primeiro importe-o da biblioteca utilitária de testes:

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

import {ComponentFixtureAutoDetect} from '@angular/core/testing';import {ComponentFixture, TestBed} from '@angular/core/testing';import {BannerComponent} from './banner.component';describe('BannerComponent (AutoChangeDetect)', () => {  let comp: BannerComponent;  let fixture: ComponentFixture<BannerComponent>;  let h1: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],    });    fixture = TestBed.createComponent(BannerComponent);    comp = fixture.componentInstance;    h1 = fixture.nativeElement.querySelector('h1');  });  it('should display original title', () => {    // Hooray! No `fixture.detectChanges()` needed    expect(h1.textContent).toContain(comp.title);  });  it('should still see original title after comp.title change', async () => {    const oldTitle = comp.title;    const newTitle = 'Test Title';    comp.title.set(newTitle);    // Displayed title is old because Angular didn't yet run change detection    expect(h1.textContent).toContain(oldTitle);    await fixture.whenStable();    expect(h1.textContent).toContain(newTitle);  });  it('should display updated title after detectChanges', () => {    comp.title.set('Test Title');    fixture.detectChanges(); // detect changes explicitly    expect(h1.textContent).toContain(comp.title);  });});

Então adicione-o ao array providers da configuração do module de teste:

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

import {ComponentFixtureAutoDetect} from '@angular/core/testing';import {ComponentFixture, TestBed} from '@angular/core/testing';import {BannerComponent} from './banner.component';describe('BannerComponent (AutoChangeDetect)', () => {  let comp: BannerComponent;  let fixture: ComponentFixture<BannerComponent>;  let h1: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],    });    fixture = TestBed.createComponent(BannerComponent);    comp = fixture.componentInstance;    h1 = fixture.nativeElement.querySelector('h1');  });  it('should display original title', () => {    // Hooray! No `fixture.detectChanges()` needed    expect(h1.textContent).toContain(comp.title);  });  it('should still see original title after comp.title change', async () => {    const oldTitle = comp.title;    const newTitle = 'Test Title';    comp.title.set(newTitle);    // Displayed title is old because Angular didn't yet run change detection    expect(h1.textContent).toContain(oldTitle);    await fixture.whenStable();    expect(h1.textContent).toContain(newTitle);  });  it('should display updated title after detectChanges', () => {    comp.title.set('Test Title');    fixture.detectChanges(); // detect changes explicitly    expect(h1.textContent).toContain(comp.title);  });});

ÚTIL: Você também pode usar a função fixture.autoDetectChanges() se quiser apenas ativar change detection automático após fazer atualizações no estado do component do fixture. Além disso, change detection automático é ativado por padrão quando se usa provideZonelessChangeDetection e desativá-lo não é recomendado.

Aqui estão três testes que ilustram como change detection automático funciona.

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

import {ComponentFixtureAutoDetect} from '@angular/core/testing';import {ComponentFixture, TestBed} from '@angular/core/testing';import {BannerComponent} from './banner.component';describe('BannerComponent (AutoChangeDetect)', () => {  let comp: BannerComponent;  let fixture: ComponentFixture<BannerComponent>;  let h1: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],    });    fixture = TestBed.createComponent(BannerComponent);    comp = fixture.componentInstance;    h1 = fixture.nativeElement.querySelector('h1');  });  it('should display original title', () => {    // Hooray! No `fixture.detectChanges()` needed    expect(h1.textContent).toContain(comp.title);  });  it('should still see original title after comp.title change', async () => {    const oldTitle = comp.title;    const newTitle = 'Test Title';    comp.title.set(newTitle);    // Displayed title is old because Angular didn't yet run change detection    expect(h1.textContent).toContain(oldTitle);    await fixture.whenStable();    expect(h1.textContent).toContain(newTitle);  });  it('should display updated title after detectChanges', () => {    comp.title.set('Test Title');    fixture.detectChanges(); // detect changes explicitly    expect(h1.textContent).toContain(comp.title);  });});

O primeiro teste mostra o benefício de change detection automático.

O segundo e terceiro testes revelam uma limitação importante. O ambiente de testes do Angular não executa change detection sincronamente quando atualizações acontecem dentro do caso de teste que alterou o title do component. O teste deve chamar await fixture.whenStable para esperar por outra rodada de change detection.

ÚTIL: O Angular não sabe sobre atualizações diretas em valores que não são signals. A maneira mais fácil de garantir que change detection seja agendado é usar signals para valores lidos no template.

Alterar um valor de input com dispatchEvent()

Para simular entrada do usuário, encontre o elemento input e defina sua propriedade value.

Mas há um passo essencial e intermediário.

O Angular não sabe que você definiu a propriedade value do elemento input. Ele não lerá essa propriedade até que você acione o evento input do elemento chamando dispatchEvent().

O exemplo seguinte demonstra a sequência adequada.

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

import {HttpClient, HttpHandler, provideHttpClient} from '@angular/common/http';import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';import {fakeAsync, TestBed, tick} from '@angular/core/testing';import {provideRouter, Router} from '@angular/router';import {RouterTestingHarness} from '@angular/router/testing';import {asyncData, click} from '../../testing';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailComponent} from './hero-detail.component';import {HeroDetailService} from './hero-detail.service';import {HeroListComponent} from './hero-list.component';////// Testing Vars //////let component: HeroDetailComponent;let harness: RouterTestingHarness;let page: Page;////// Tests //////describe('HeroDetailComponent', () => {  describe('with HeroModule setup', heroModuleSetup);  describe('when override its provided HeroDetailService', overrideSetup);  describe('with FormsModule setup', formsModuleSetup);  describe('with SharedModule setup', sharedModuleSetup);});///////////////////const testHero = getTestHeroes()[0];function overrideSetup() {  class HeroDetailServiceSpy {    testHero: Hero = {...testHero};    /* emit cloned test hero */    getHero = jasmine      .createSpy('getHero')      .and.callFake(() => asyncData(Object.assign({}, this.testHero)));    /* emit clone of test hero, with changes merged in */    saveHero = jasmine      .createSpy('saveHero')      .and.callFake((hero: Hero) => asyncData(Object.assign(this.testHero, hero)));  }  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes', component: HeroListComponent},            {path: 'heroes/:id', component: HeroDetailComponent},          ]),          HttpClient,          HttpHandler,          // HeroDetailService at this level is IRRELEVANT!          {provide: HeroDetailService, useValue: {}},        ],      }),    )      .overrideComponent(HeroDetailComponent, {        set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]},      });  });  let hdsSpy: HeroDetailServiceSpy;  beforeEach(async () => {    harness = await RouterTestingHarness.create();    component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetailComponent);    page = new Page();    // get the component's injected HeroDetailServiceSpy    hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any;    harness.detectChanges();  });  it('should have called `getHero`', () => {    expect(hdsSpy.getHero.calls.count())      .withContext('getHero called once')      .toBe(1, 'getHero called once');  });  it("should display stub hero's name", () => {    expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);  });  it('should save stub hero change', fakeAsync(() => {    const origName = hdsSpy.testHero.name;    const newName = 'New Name';    page.nameInput.value = newName;    page.nameInput.dispatchEvent(new Event('input')); // tell Angular    expect(component.hero.name).withContext('component hero has new name').toBe(newName);    expect(hdsSpy.testHero.name).withContext('service hero unchanged before save').toBe(origName);    click(page.saveBtn);    expect(hdsSpy.saveHero.calls.count()).withContext('saveHero called once').toBe(1);    tick(); // wait for async save to complete    expect(hdsSpy.testHero.name).withContext('service hero has new name after save').toBe(newName);    expect(TestBed.inject(Router).url).toEqual('/heroes');  }));}////////////////////import {getTestHeroes} from '../model/testing/test-hero.service';const firstHero = getTestHeroes()[0];function heroModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes/:id', component: HeroDetailComponent},            {path: 'heroes', component: HeroListComponent},          ]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  describe('when navigate to existing hero', () => {    let expectedHero: Hero;    beforeEach(async () => {      expectedHero = firstHero;      await createComponent(expectedHero.id);    });    it("should display that hero's name", () => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });    it('should navigate when click cancel', () => {      click(page.cancelBtn);      expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`);    });    it('should save when click save but not navigate immediately', () => {      click(page.saveBtn);      expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'}));      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    });    it('should navigate when click save and save resolves', fakeAsync(() => {      click(page.saveBtn);      tick(); // wait for async save to complete      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    }));    it('should convert hero name to Title Case', async () => {      harness.fixture.autoDetectChanges();      // get the name's input and display elements from the DOM      const hostElement: HTMLElement = harness.routeNativeElement!;      const nameInput: HTMLInputElement = hostElement.querySelector('input')!;      const nameDisplay: HTMLElement = hostElement.querySelector('span')!;      // simulate user entering a new name into the input box      nameInput.value = 'quick BROWN  fOx';      // Dispatch a DOM event so that Angular learns of input value change.      nameInput.dispatchEvent(new Event('input'));      // Wait for Angular to update the display binding through the title pipe      await harness.fixture.whenStable();      expect(nameDisplay.textContent).toBe('Quick Brown  Fox');    });  });  describe('when navigate to non-existent hero id', () => {    beforeEach(async () => {      await createComponent(999);    });    it('should try to navigate back to hero list', () => {      expect(TestBed.inject(Router).url).toEqual('/heroes');    });  });}/////////////////////import {FormsModule} from '@angular/forms';import {TitleCasePipe} from '../shared/title-case.pipe';import {appConfig} from '../app.config';function formsModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [FormsModule, HeroDetailComponent, TitleCasePipe],        providers: [          provideHttpClient(),          provideHttpClientTesting(),          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}///////////////////////function sharedModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, sharedImports],        providers: [          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}/////////// Helpers //////** Create the HeroDetailComponent, initialize it, set test variables  */async function createComponent(id: number) {  harness = await RouterTestingHarness.create();  component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetailComponent);  page = new Page();  const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`);  const hero = getTestHeroes().find((h) => h.id === Number(id));  request.flush(hero ? [hero] : []);  harness.detectChanges();}class Page {  // getter properties wait to query the DOM until called.  get buttons() {    return this.queryAll<HTMLButtonElement>('button');  }  get saveBtn() {    return this.buttons[0];  }  get cancelBtn() {    return this.buttons[1];  }  get nameDisplay() {    return this.query<HTMLElement>('span');  }  get nameInput() {    return this.query<HTMLInputElement>('input');  }  //// query helpers ////  private query<T>(selector: string): T {    return harness.routeNativeElement!.querySelector(selector)! as T;  }  private queryAll<T>(selector: string): T[] {    return harness.routeNativeElement!.querySelectorAll(selector) as any as T[];  }}

Component com arquivos externos

O BannerComponent anterior é definido com um template inline e css inline, especificados nas propriedades @Component.template e @Component.styles respectivamente.

Muitos components especificam templates externos e css externo com as propriedades @Component.templateUrl e @Component.styleUrls respectivamente, como a seguinte variante de BannerComponent faz.

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

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

Esta sintaxe diz ao compilador Angular para ler os arquivos externos durante a compilação do component.

Isso não é um problema quando você executa o comando ng test da CLI porque ele compila a aplicação antes de executar os testes.

No entanto, se você executar os testes em um ambiente não-CLI, testes deste component podem falhar. Por exemplo, se você executar os testes de BannerComponent em um ambiente de codificação web como plunker, você verá uma mensagem como esta:

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

Você obtém esta mensagem de falha de teste quando o ambiente de runtime compila o código fonte durante os próprios testes.

Para corrigir o problema, chame compileComponents().

Component com uma dependência

Components frequentemente têm dependências de services.

O WelcomeComponent exibe uma mensagem de boas-vindas ao usuário logado. Ele sabe quem é o usuário com base em uma propriedade do UserService injetado:

app/welcome/welcome.component.ts

import {Component, inject, OnInit, signal} from '@angular/core';import {UserService} from '../model/user.service';@Component({  selector: 'app-welcome',  template: '<h3 class="welcome"><i>{{welcome()}}</i></h3>',})export class WelcomeComponent {  welcome = signal('');  private userService = inject(UserService);  constructor() {    this.welcome.set(      this.userService.isLoggedIn() ? 'Welcome, ' + this.userService.user().name : 'Please log in.',    );  }}

O WelcomeComponent tem lógica de decisão que interage com o service, lógica que torna este component digno de teste.

Fornecer test doubles de service

Um component-under-test não precisa receber services reais.

Injetar o UserService real poderia ser difícil. O service real pode pedir credenciais de login do usuário e tentar alcançar um servidor de autenticação. Esses comportamentos podem ser difíceis de interceptar. Esteja ciente de que usar test doubles faz o teste se comportar de forma diferente da produção, então use-os com moderação.

Obter services injetados

Os testes precisam de acesso ao UserService injetado no WelcomeComponent.

O Angular tem um sistema de injeção hierárquico. Pode haver injectors em múltiplos níveis, do injector raiz criado pelo TestBed até a árvore de components.

A maneira mais segura de obter o service injetado, a maneira que sempre funciona, é obtê-lo do injector do component-under-test. O injector do component é uma propriedade do DebugElement do fixture.

WelcomeComponent's injector

import {ComponentFixture, inject, TestBed} from '@angular/core/testing';import {UserService} from '../model/user.service';import {WelcomeComponent} from './welcome.component';class MockUserService {  isLoggedIn = true;  user = {name: 'Test User'};}describe('WelcomeComponent', () => {  let comp: WelcomeComponent;  let fixture: ComponentFixture<WelcomeComponent>;  let componentUserService: UserService; // the actually injected service  let userService: UserService; // the TestBed injected service  let el: HTMLElement; // the DOM element with the welcome message  beforeEach(() => {    fixture = TestBed.createComponent(WelcomeComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // UserService actually injected into the component    userService = fixture.debugElement.injector.get(UserService);    componentUserService = userService;    // UserService from the root injector    userService = TestBed.inject(UserService);    //  get the "welcome" element by CSS selector (e.g., by class name)    el = fixture.nativeElement.querySelector('.welcome');  });  it('should welcome the user', async () => {    await fixture.whenStable();    const content = el.textContent;    expect(content).withContext('"Welcome ..."').toContain('Welcome');    expect(content).withContext('expected name').toContain('Test User');  });  it('should welcome "Bubba"', async () => {    userService.user.set({name: 'Bubba'}); // welcome message hasn't been shown yet    await fixture.whenStable();    expect(el.textContent).toContain('Bubba');  });  it('should request login if not logged in', async () => {    userService.isLoggedIn.set(false); // welcome message hasn't been shown yet    await fixture.whenStable();    const content = el.textContent;    expect(content).withContext('not welcomed').not.toContain('Welcome');    expect(content)      .withContext('"log in"')      .toMatch(/log in/i);  });  it("should inject the component's UserService instance", inject(    [UserService],    (service: UserService) => {      expect(service).toBe(componentUserService);    },  ));  it('TestBed and Component UserService should be the same', () => {    expect(userService).toBe(componentUserService);  });});

ÚTIL: Isso geralmente não é necessário. Services são frequentemente fornecidos na raiz ou o TestBed os sobrescreve e podem ser recuperados mais facilmente com TestBed.inject() (veja abaixo).

TestBed.inject()

Isso é mais fácil de lembrar e menos verboso do que recuperar um service usando o DebugElement do fixture.

Neste conjunto de testes, o único provider de UserService é o module de teste raiz, então é seguro chamar TestBed.inject() da seguinte forma:

TestBed injector

import {ComponentFixture, inject, TestBed} from '@angular/core/testing';import {UserService} from '../model/user.service';import {WelcomeComponent} from './welcome.component';class MockUserService {  isLoggedIn = true;  user = {name: 'Test User'};}describe('WelcomeComponent', () => {  let comp: WelcomeComponent;  let fixture: ComponentFixture<WelcomeComponent>;  let componentUserService: UserService; // the actually injected service  let userService: UserService; // the TestBed injected service  let el: HTMLElement; // the DOM element with the welcome message  beforeEach(() => {    fixture = TestBed.createComponent(WelcomeComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // UserService actually injected into the component    userService = fixture.debugElement.injector.get(UserService);    componentUserService = userService;    // UserService from the root injector    userService = TestBed.inject(UserService);    //  get the "welcome" element by CSS selector (e.g., by class name)    el = fixture.nativeElement.querySelector('.welcome');  });  it('should welcome the user', async () => {    await fixture.whenStable();    const content = el.textContent;    expect(content).withContext('"Welcome ..."').toContain('Welcome');    expect(content).withContext('expected name').toContain('Test User');  });  it('should welcome "Bubba"', async () => {    userService.user.set({name: 'Bubba'}); // welcome message hasn't been shown yet    await fixture.whenStable();    expect(el.textContent).toContain('Bubba');  });  it('should request login if not logged in', async () => {    userService.isLoggedIn.set(false); // welcome message hasn't been shown yet    await fixture.whenStable();    const content = el.textContent;    expect(content).withContext('not welcomed').not.toContain('Welcome');    expect(content)      .withContext('"log in"')      .toMatch(/log in/i);  });  it("should inject the component's UserService instance", inject(    [UserService],    (service: UserService) => {      expect(service).toBe(componentUserService);    },  ));  it('TestBed and Component UserService should be the same', () => {    expect(userService).toBe(componentUserService);  });});

ÚTIL: Para um caso de uso em que TestBed.inject() não funciona, veja a seção Override component providers que explica quando e por que você deve obter o service do injector do component.

Configuração final e testes

Aqui está o beforeEach() completo, usando TestBed.inject():

app/welcome/welcome.component.spec.ts

import {ComponentFixture, inject, TestBed} from '@angular/core/testing';import {UserService} from '../model/user.service';import {WelcomeComponent} from './welcome.component';class MockUserService {  isLoggedIn = true;  user = {name: 'Test User'};}describe('WelcomeComponent', () => {  let comp: WelcomeComponent;  let fixture: ComponentFixture<WelcomeComponent>;  let componentUserService: UserService; // the actually injected service  let userService: UserService; // the TestBed injected service  let el: HTMLElement; // the DOM element with the welcome message  beforeEach(() => {    fixture = TestBed.createComponent(WelcomeComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // UserService actually injected into the component    userService = fixture.debugElement.injector.get(UserService);    componentUserService = userService;    // UserService from the root injector    userService = TestBed.inject(UserService);    //  get the "welcome" element by CSS selector (e.g., by class name)    el = fixture.nativeElement.querySelector('.welcome');  });  it('should welcome the user', async () => {    await fixture.whenStable();    const content = el.textContent;    expect(content).withContext('"Welcome ..."').toContain('Welcome');    expect(content).withContext('expected name').toContain('Test User');  });  it('should welcome "Bubba"', async () => {    userService.user.set({name: 'Bubba'}); // welcome message hasn't been shown yet    await fixture.whenStable();    expect(el.textContent).toContain('Bubba');  });  it('should request login if not logged in', async () => {    userService.isLoggedIn.set(false); // welcome message hasn't been shown yet    await fixture.whenStable();    const content = el.textContent;    expect(content).withContext('not welcomed').not.toContain('Welcome');    expect(content)      .withContext('"log in"')      .toMatch(/log in/i);  });  it("should inject the component's UserService instance", inject(    [UserService],    (service: UserService) => {      expect(service).toBe(componentUserService);    },  ));  it('TestBed and Component UserService should be the same', () => {    expect(userService).toBe(componentUserService);  });});

E aqui estão alguns testes:

app/welcome/welcome.component.spec.ts

import {ComponentFixture, inject, TestBed} from '@angular/core/testing';import {UserService} from '../model/user.service';import {WelcomeComponent} from './welcome.component';class MockUserService {  isLoggedIn = true;  user = {name: 'Test User'};}describe('WelcomeComponent', () => {  let comp: WelcomeComponent;  let fixture: ComponentFixture<WelcomeComponent>;  let componentUserService: UserService; // the actually injected service  let userService: UserService; // the TestBed injected service  let el: HTMLElement; // the DOM element with the welcome message  beforeEach(() => {    fixture = TestBed.createComponent(WelcomeComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // UserService actually injected into the component    userService = fixture.debugElement.injector.get(UserService);    componentUserService = userService;    // UserService from the root injector    userService = TestBed.inject(UserService);    //  get the "welcome" element by CSS selector (e.g., by class name)    el = fixture.nativeElement.querySelector('.welcome');  });  it('should welcome the user', async () => {    await fixture.whenStable();    const content = el.textContent;    expect(content).withContext('"Welcome ..."').toContain('Welcome');    expect(content).withContext('expected name').toContain('Test User');  });  it('should welcome "Bubba"', async () => {    userService.user.set({name: 'Bubba'}); // welcome message hasn't been shown yet    await fixture.whenStable();    expect(el.textContent).toContain('Bubba');  });  it('should request login if not logged in', async () => {    userService.isLoggedIn.set(false); // welcome message hasn't been shown yet    await fixture.whenStable();    const content = el.textContent;    expect(content).withContext('not welcomed').not.toContain('Welcome');    expect(content)      .withContext('"log in"')      .toMatch(/log in/i);  });  it("should inject the component's UserService instance", inject(    [UserService],    (service: UserService) => {      expect(service).toBe(componentUserService);    },  ));  it('TestBed and Component UserService should be the same', () => {    expect(userService).toBe(componentUserService);  });});

O primeiro é um teste de sanidade; ele confirma que o UserService é chamado e está funcionando.

ÚTIL: A função withContext (por exemplo, 'expected name') é um rótulo de falha opcional. Se a expectation falhar, o Jasmine anexa este rótulo à mensagem de falha da expectation. Em uma spec com múltiplas expectations, pode ajudar a esclarecer o que deu errado e qual expectation falhou.

Os testes restantes confirmam a lógica do component quando o service retorna valores diferentes. O segundo teste valida o efeito de alterar o nome do usuário. O terceiro teste verifica que o component exibe a mensagem adequada quando não há usuário logado.

Component com service assíncrono

Neste exemplo, o template AboutComponent hospeda um TwainComponent. O TwainComponent exibe citações de Mark Twain.

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

import {Component, inject, OnInit, signal} from '@angular/core';import {AsyncPipe} from '@angular/common';import {sharedImports} from '../shared/shared';import {Observable, of} from 'rxjs';import {catchError, startWith} from 'rxjs/operators';import {TwainService} from './twain.service';@Component({  selector: 'twain-quote',  template: ` <p class="twain">      <i>{{ quote | async }}</i>    </p>    <button type="button" (click)="getQuote()">Next quote</button>    @if (errorMessage()) {      <p class="error">{{ errorMessage() }}</p>    }`,  styles: ['.twain { font-style: italic; } .error { color: red; }'],  imports: [AsyncPipe, sharedImports],})export class TwainComponent {  errorMessage = signal('');  quote?: Observable<string>;  private twainService = inject(TwainService);  constructor() {    this.getQuote();  }  getQuote() {    this.errorMessage.set('');    this.quote = this.twainService.getQuote().pipe(      startWith('...'),      catchError((err: any) => {        this.errorMessage.set(err.message || err.toString());        return of('...'); // reset message to placeholder      }),    );  }}

ÚTIL: O valor da propriedade quote do component passa por um AsyncPipe. Isso significa que a propriedade retorna ou uma Promise ou um Observable.

Neste exemplo, o método TwainComponent.getQuote() diz que a propriedade quote retorna um Observable.

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

import {Component, inject, OnInit, signal} from '@angular/core';import {AsyncPipe} from '@angular/common';import {sharedImports} from '../shared/shared';import {Observable, of} from 'rxjs';import {catchError, startWith} from 'rxjs/operators';import {TwainService} from './twain.service';@Component({  selector: 'twain-quote',  template: ` <p class="twain">      <i>{{ quote | async }}</i>    </p>    <button type="button" (click)="getQuote()">Next quote</button>    @if (errorMessage()) {      <p class="error">{{ errorMessage() }}</p>    }`,  styles: ['.twain { font-style: italic; } .error { color: red; }'],  imports: [AsyncPipe, sharedImports],})export class TwainComponent {  errorMessage = signal('');  quote?: Observable<string>;  private twainService = inject(TwainService);  constructor() {    this.getQuote();  }  getQuote() {    this.errorMessage.set('');    this.quote = this.twainService.getQuote().pipe(      startWith('...'),      catchError((err: any) => {        this.errorMessage.set(err.message || err.toString());        return of('...'); // reset message to placeholder      }),    );  }}

O TwainComponent obtém citações de um TwainService injetado. O component inicia o Observable retornado com um valor placeholder ('...'), antes que o service possa retornar sua primeira citação.

O catchError intercepta erros do service, prepara uma mensagem de erro e retorna o valor placeholder no canal de sucesso.

Estes são todos recursos que você vai querer testar.

Testes com um spy

Ao testar um component, apenas a API pública do service deve importar. Em geral, os próprios testes não devem fazer chamadas a servidores remotos. Eles devem emular tais chamadas. A configuração neste app/twain/twain.component.spec.ts mostra uma maneira de fazer isso:

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

import {fakeAsync, ComponentFixture, TestBed, tick, waitForAsync} from '@angular/core/testing';import {asyncData, asyncError} from '../../testing';import {Subject, defer, of, throwError} from 'rxjs';import {last} from 'rxjs/operators';import {TwainComponent} from './twain.component';import {TwainService} from './twain.service';describe('TwainComponent', () => {  let component: TwainComponent;  let fixture: ComponentFixture<TwainComponent>;  let getQuoteSpy: jasmine.Spy;  let quoteEl: HTMLElement;  let testQuote: string;  // Helper function to get the error message element value  // An *ngIf keeps it out of the DOM until there is an error  const errorMessage = () => {    const el = fixture.nativeElement.querySelector('.error');    return el ? el.textContent : null;  };  beforeEach(() => {    TestBed.configureTestingModule({      providers: [TwainService],    });    testQuote = 'Test Quote';    // Create a fake TwainService object with a `getQuote()` spy    const twainService = TestBed.inject(TwainService);    // Make the spy return a synchronous Observable with the test data    getQuoteSpy = spyOn(twainService, 'getQuote').and.returnValue(of(testQuote));    fixture = TestBed.createComponent(TwainComponent);    fixture.autoDetectChanges();    component = fixture.componentInstance;    quoteEl = fixture.nativeElement.querySelector('.twain');  });  describe('when test with synchronous observable', () => {    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    // The quote would not be immediately available if the service were truly async.    it('should show quote after component initialized', async () => {      await fixture.whenStable(); // onInit()      // sync spy result shows testQuote immediately after init      expect(quoteEl.textContent).toBe(testQuote);      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    // The error would not be immediately available if the service were truly async.    // Use `fakeAsync` because the component error calls `setTimeout`    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an error observable after a timeout      getQuoteSpy.and.returnValue(        defer(() => {          return new Promise((resolve, reject) => {            setTimeout(() => {              reject('TwainService test failure');            });          });        }),      );      fixture.detectChanges(); // onInit()      // sync spy errors immediately after init      tick(); // flush the setTimeout()      fixture.detectChanges(); // update errorMessage within setTimeout()      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });  describe('when test with asynchronous observable', () => {    beforeEach(() => {      // Simulate delayed observable values with the `asyncData()` helper      getQuoteSpy.and.returnValue(asyncData(testQuote));    });    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    it('should still not show quote after component initialized', () => {      fixture.detectChanges();      // getQuote service is async => still has not returned with quote      // so should show the start value, '...'      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      expect(errorMessage()).withContext('should not show error').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      tick(); // flush the observable to get the quote      fixture.detectChanges(); // update view      expect(quoteEl.textContent).withContext('should show quote').toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    }));    it('should show quote after getQuote (async)', async () => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      await fixture.whenStable();      // wait for async getQuote      fixture.detectChanges(); // update view with quote      expect(quoteEl.textContent).toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    });    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an async error observable      getQuoteSpy.and.returnValue(asyncError<string>('TwainService test failure'));      fixture.detectChanges();      tick(); // component shows error after a setTimeout()      fixture.detectChanges(); // update error message      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });});

Foque no spy.

import {fakeAsync, ComponentFixture, TestBed, tick, waitForAsync} from '@angular/core/testing';import {asyncData, asyncError} from '../../testing';import {Subject, defer, of, throwError} from 'rxjs';import {last} from 'rxjs/operators';import {TwainComponent} from './twain.component';import {TwainService} from './twain.service';describe('TwainComponent', () => {  let component: TwainComponent;  let fixture: ComponentFixture<TwainComponent>;  let getQuoteSpy: jasmine.Spy;  let quoteEl: HTMLElement;  let testQuote: string;  // Helper function to get the error message element value  // An *ngIf keeps it out of the DOM until there is an error  const errorMessage = () => {    const el = fixture.nativeElement.querySelector('.error');    return el ? el.textContent : null;  };  beforeEach(() => {    TestBed.configureTestingModule({      providers: [TwainService],    });    testQuote = 'Test Quote';    // Create a fake TwainService object with a `getQuote()` spy    const twainService = TestBed.inject(TwainService);    // Make the spy return a synchronous Observable with the test data    getQuoteSpy = spyOn(twainService, 'getQuote').and.returnValue(of(testQuote));    fixture = TestBed.createComponent(TwainComponent);    fixture.autoDetectChanges();    component = fixture.componentInstance;    quoteEl = fixture.nativeElement.querySelector('.twain');  });  describe('when test with synchronous observable', () => {    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    // The quote would not be immediately available if the service were truly async.    it('should show quote after component initialized', async () => {      await fixture.whenStable(); // onInit()      // sync spy result shows testQuote immediately after init      expect(quoteEl.textContent).toBe(testQuote);      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    // The error would not be immediately available if the service were truly async.    // Use `fakeAsync` because the component error calls `setTimeout`    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an error observable after a timeout      getQuoteSpy.and.returnValue(        defer(() => {          return new Promise((resolve, reject) => {            setTimeout(() => {              reject('TwainService test failure');            });          });        }),      );      fixture.detectChanges(); // onInit()      // sync spy errors immediately after init      tick(); // flush the setTimeout()      fixture.detectChanges(); // update errorMessage within setTimeout()      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });  describe('when test with asynchronous observable', () => {    beforeEach(() => {      // Simulate delayed observable values with the `asyncData()` helper      getQuoteSpy.and.returnValue(asyncData(testQuote));    });    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    it('should still not show quote after component initialized', () => {      fixture.detectChanges();      // getQuote service is async => still has not returned with quote      // so should show the start value, '...'      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      expect(errorMessage()).withContext('should not show error').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      tick(); // flush the observable to get the quote      fixture.detectChanges(); // update view      expect(quoteEl.textContent).withContext('should show quote').toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    }));    it('should show quote after getQuote (async)', async () => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      await fixture.whenStable();      // wait for async getQuote      fixture.detectChanges(); // update view with quote      expect(quoteEl.textContent).toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    });    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an async error observable      getQuoteSpy.and.returnValue(asyncError<string>('TwainService test failure'));      fixture.detectChanges();      tick(); // component shows error after a setTimeout()      fixture.detectChanges(); // update error message      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });});

O spy é projetado de modo que qualquer chamada a getQuote recebe um observable com uma citação de teste. Diferentemente do método getQuote() real, este spy ignora o servidor e retorna um observable síncrono cujo valor está disponível imediatamente.

Você pode escrever muitos testes úteis com este spy, mesmo que seu Observable seja síncrono.

ÚTIL: É melhor limitar o uso de spies apenas ao que é necessário para o teste. Criar mocks ou spies para mais do que o necessário pode ser frágil. À medida que o component e o injectable evoluem, os testes não relacionados podem falhar porque não simulam mais comportamentos suficientes que de outra forma não afetariam o teste.

Teste assíncrono com fakeAsync()

Para usar a funcionalidade fakeAsync(), você deve importar zone.js/testing no seu arquivo de configuração de teste. Se você criou seu projeto com a Angular CLI, zone-testing já está importado em src/test.ts.

O teste seguinte confirma o comportamento esperado quando o service retorna um ErrorObservable.

import {fakeAsync, ComponentFixture, TestBed, tick, waitForAsync} from '@angular/core/testing';import {asyncData, asyncError} from '../../testing';import {Subject, defer, of, throwError} from 'rxjs';import {last} from 'rxjs/operators';import {TwainComponent} from './twain.component';import {TwainService} from './twain.service';describe('TwainComponent', () => {  let component: TwainComponent;  let fixture: ComponentFixture<TwainComponent>;  let getQuoteSpy: jasmine.Spy;  let quoteEl: HTMLElement;  let testQuote: string;  // Helper function to get the error message element value  // An *ngIf keeps it out of the DOM until there is an error  const errorMessage = () => {    const el = fixture.nativeElement.querySelector('.error');    return el ? el.textContent : null;  };  beforeEach(() => {    TestBed.configureTestingModule({      providers: [TwainService],    });    testQuote = 'Test Quote';    // Create a fake TwainService object with a `getQuote()` spy    const twainService = TestBed.inject(TwainService);    // Make the spy return a synchronous Observable with the test data    getQuoteSpy = spyOn(twainService, 'getQuote').and.returnValue(of(testQuote));    fixture = TestBed.createComponent(TwainComponent);    fixture.autoDetectChanges();    component = fixture.componentInstance;    quoteEl = fixture.nativeElement.querySelector('.twain');  });  describe('when test with synchronous observable', () => {    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    // The quote would not be immediately available if the service were truly async.    it('should show quote after component initialized', async () => {      await fixture.whenStable(); // onInit()      // sync spy result shows testQuote immediately after init      expect(quoteEl.textContent).toBe(testQuote);      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    // The error would not be immediately available if the service were truly async.    // Use `fakeAsync` because the component error calls `setTimeout`    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an error observable after a timeout      getQuoteSpy.and.returnValue(        defer(() => {          return new Promise((resolve, reject) => {            setTimeout(() => {              reject('TwainService test failure');            });          });        }),      );      fixture.detectChanges(); // onInit()      // sync spy errors immediately after init      tick(); // flush the setTimeout()      fixture.detectChanges(); // update errorMessage within setTimeout()      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });  describe('when test with asynchronous observable', () => {    beforeEach(() => {      // Simulate delayed observable values with the `asyncData()` helper      getQuoteSpy.and.returnValue(asyncData(testQuote));    });    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    it('should still not show quote after component initialized', () => {      fixture.detectChanges();      // getQuote service is async => still has not returned with quote      // so should show the start value, '...'      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      expect(errorMessage()).withContext('should not show error').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      tick(); // flush the observable to get the quote      fixture.detectChanges(); // update view      expect(quoteEl.textContent).withContext('should show quote').toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    }));    it('should show quote after getQuote (async)', async () => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      await fixture.whenStable();      // wait for async getQuote      fixture.detectChanges(); // update view with quote      expect(quoteEl.textContent).toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    });    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an async error observable      getQuoteSpy.and.returnValue(asyncError<string>('TwainService test failure'));      fixture.detectChanges();      tick(); // component shows error after a setTimeout()      fixture.detectChanges(); // update error message      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });});

ÚTIL: A função it() recebe um argumento da seguinte forma.

fakeAsync(() => { /_test body_/ })

A função fakeAsync() habilita um estilo de codificação linear executando o corpo do teste em uma fakeAsync test zone especial. O corpo do teste parece ser síncrono. Não há sintaxe aninhada (como um Promise.then()) para interromper o fluxo de controle.

ÚTIL: Limitação: A função fakeAsync() não funcionará se o corpo do teste fizer uma chamada XMLHttpRequest (XHR). Chamadas XHR dentro de um teste são raras, mas se você precisar chamar XHR, use waitForAsync().

IMPORTANTE: Esteja ciente de que tarefas assíncronas que acontecem dentro da zona fakeAsync precisam ser executadas manualmente com flush ou tick. Se você tentar aguardar que elas sejam concluídas (ou seja, usando fixture.whenStable) sem usar os helpers de teste fakeAsync para avançar o tempo, seu teste provavelmente falhará. Veja abaixo para mais informações.

A função tick()

Você precisa chamar tick() para avançar o relógio virtual.

Chamar tick() simula a passagem do tempo até que todas as atividades assíncronas pendentes terminem. Neste caso, ele espera pelo setTimeout() do observable.

A função tick() aceita millis e tickOptions como parâmetros. O parâmetro millis especifica quanto o relógio virtual avança e o padrão é 0 se não fornecido. Por exemplo, se você tiver um setTimeout(fn, 100) em um teste fakeAsync(), você precisa usar tick(100) para acionar o callback fn. O parâmetro tickOptions opcional tem uma propriedade chamada processNewMacroTasksSynchronously. A propriedade processNewMacroTasksSynchronously representa se deve invocar novas macro tasks geradas ao fazer tick e o padrão é true.

import {fakeAsync, tick, waitForAsync} from '@angular/core/testing';import {interval, of} from 'rxjs';import {delay, take} from 'rxjs/operators';describe('Angular async helper', () => {  describe('async', () => {    let actuallyDone = false;    beforeEach(() => {      actuallyDone = false;    });    afterEach(() => {      expect(actuallyDone).withContext('actuallyDone should be true').toBe(true);    });    it('should run normal test', () => {      actuallyDone = true;    });    it('should run normal async test', (done: DoneFn) => {      setTimeout(() => {        actuallyDone = true;        done();      }, 0);    });    it('should run async test with task', waitForAsync(() => {      setTimeout(() => {        actuallyDone = true;      }, 0);    }));    it('should run async test with task', waitForAsync(() => {      const id = setInterval(() => {        actuallyDone = true;        clearInterval(id);      }, 100);    }));    it('should run async test with successful promise', waitForAsync(() => {      const p = new Promise((resolve) => {        setTimeout(resolve, 10);      });      p.then(() => {        actuallyDone = true;      });    }));    it('should run async test with failed promise', waitForAsync(() => {      const p = new Promise((resolve, reject) => {        setTimeout(reject, 10);      });      p.catch(() => {        actuallyDone = true;      });    }));    // Use done. Can also use async or fakeAsync.    it('should run async test with successful delayed Observable', (done: DoneFn) => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),        complete: done,      });    });    it('should run async test with successful delayed Observable', waitForAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });    }));    it('should run async test with successful delayed Observable', fakeAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });      tick(10);    }));  });  describe('fakeAsync', () => {    it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {      let called = false;      setTimeout(() => {        called = true;      }, 100);      tick(100);      expect(called).toBe(true);    }));    it('should run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0);      // the nested timeout will also be triggered      expect(callback).toHaveBeenCalled();    }));    it('should not run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0, {processNewMacroTasksSynchronously: false});      // the nested timeout will not be triggered      expect(callback).not.toHaveBeenCalled();      tick(0);      expect(callback).toHaveBeenCalled();    }));    it('should get Date diff correctly in fakeAsync', fakeAsync(() => {      const start = Date.now();      tick(100);      const end = Date.now();      expect(end - start).toBe(100);    }));    it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {      // need to add `import 'zone.js/plugins/zone-patch-rxjs-fake-async'      // to patch rxjs scheduler      let result = '';      of('hello')        .pipe(delay(1000))        .subscribe((v) => {          result = v;        });      expect(result).toBe('');      tick(1000);      expect(result).toBe('hello');      const start = new Date().getTime();      let dateDiff = 0;      interval(1000)        .pipe(take(2))        .subscribe(() => (dateDiff = new Date().getTime() - start));      tick(1000);      expect(dateDiff).toBe(1000);      tick(1000);      expect(dateDiff).toBe(2000);    }));  });  describe('use jasmine.clock()', () => {    // need to config __zone_symbol__fakeAsyncPatchLock flag    // before loading zone.js/testing    beforeEach(() => {      jasmine.clock().install();    });    afterEach(() => {      jasmine.clock().uninstall();    });    it('should auto enter fakeAsync', () => {      // is in fakeAsync now, don't need to call fakeAsync(testFn)      let called = false;      setTimeout(() => {        called = true;      }, 100);      jasmine.clock().tick(100);      expect(called).toBe(true);    });  });  describe('test jsonp', () => {    function jsonp(url: string, callback: () => void) {      // do a jsonp call which is not zone aware    }    // need to config __zone_symbol__supportWaitUnResolvedChainedPromise flag    // before loading zone.js/testing    it('should wait until promise.then is called', waitForAsync(() => {      let finished = false;      new Promise<void>((res) => {        jsonp('localhost:8080/jsonp', () => {          // success callback and resolve the promise          finished = true;          res();        });      }).then(() => {        // async will wait until promise.then is called        // if __zone_symbol__supportWaitUnResolvedChainedPromise is set        expect(finished).toBe(true);      });    }));  });});

A função tick() é um dos utilitários de teste do Angular que você importa com TestBed. É uma companheira de fakeAsync() e você só pode chamá-la dentro de um corpo fakeAsync().

tickOptions

Neste exemplo, você tem uma nova macro task, a função setTimeout aninhada. Por padrão, quando o tick é setTimeout, outside e nested serão ambos acionados.

import {fakeAsync, tick, waitForAsync} from '@angular/core/testing';import {interval, of} from 'rxjs';import {delay, take} from 'rxjs/operators';describe('Angular async helper', () => {  describe('async', () => {    let actuallyDone = false;    beforeEach(() => {      actuallyDone = false;    });    afterEach(() => {      expect(actuallyDone).withContext('actuallyDone should be true').toBe(true);    });    it('should run normal test', () => {      actuallyDone = true;    });    it('should run normal async test', (done: DoneFn) => {      setTimeout(() => {        actuallyDone = true;        done();      }, 0);    });    it('should run async test with task', waitForAsync(() => {      setTimeout(() => {        actuallyDone = true;      }, 0);    }));    it('should run async test with task', waitForAsync(() => {      const id = setInterval(() => {        actuallyDone = true;        clearInterval(id);      }, 100);    }));    it('should run async test with successful promise', waitForAsync(() => {      const p = new Promise((resolve) => {        setTimeout(resolve, 10);      });      p.then(() => {        actuallyDone = true;      });    }));    it('should run async test with failed promise', waitForAsync(() => {      const p = new Promise((resolve, reject) => {        setTimeout(reject, 10);      });      p.catch(() => {        actuallyDone = true;      });    }));    // Use done. Can also use async or fakeAsync.    it('should run async test with successful delayed Observable', (done: DoneFn) => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),        complete: done,      });    });    it('should run async test with successful delayed Observable', waitForAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });    }));    it('should run async test with successful delayed Observable', fakeAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });      tick(10);    }));  });  describe('fakeAsync', () => {    it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {      let called = false;      setTimeout(() => {        called = true;      }, 100);      tick(100);      expect(called).toBe(true);    }));    it('should run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0);      // the nested timeout will also be triggered      expect(callback).toHaveBeenCalled();    }));    it('should not run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0, {processNewMacroTasksSynchronously: false});      // the nested timeout will not be triggered      expect(callback).not.toHaveBeenCalled();      tick(0);      expect(callback).toHaveBeenCalled();    }));    it('should get Date diff correctly in fakeAsync', fakeAsync(() => {      const start = Date.now();      tick(100);      const end = Date.now();      expect(end - start).toBe(100);    }));    it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {      // need to add `import 'zone.js/plugins/zone-patch-rxjs-fake-async'      // to patch rxjs scheduler      let result = '';      of('hello')        .pipe(delay(1000))        .subscribe((v) => {          result = v;        });      expect(result).toBe('');      tick(1000);      expect(result).toBe('hello');      const start = new Date().getTime();      let dateDiff = 0;      interval(1000)        .pipe(take(2))        .subscribe(() => (dateDiff = new Date().getTime() - start));      tick(1000);      expect(dateDiff).toBe(1000);      tick(1000);      expect(dateDiff).toBe(2000);    }));  });  describe('use jasmine.clock()', () => {    // need to config __zone_symbol__fakeAsyncPatchLock flag    // before loading zone.js/testing    beforeEach(() => {      jasmine.clock().install();    });    afterEach(() => {      jasmine.clock().uninstall();    });    it('should auto enter fakeAsync', () => {      // is in fakeAsync now, don't need to call fakeAsync(testFn)      let called = false;      setTimeout(() => {        called = true;      }, 100);      jasmine.clock().tick(100);      expect(called).toBe(true);    });  });  describe('test jsonp', () => {    function jsonp(url: string, callback: () => void) {      // do a jsonp call which is not zone aware    }    // need to config __zone_symbol__supportWaitUnResolvedChainedPromise flag    // before loading zone.js/testing    it('should wait until promise.then is called', waitForAsync(() => {      let finished = false;      new Promise<void>((res) => {        jsonp('localhost:8080/jsonp', () => {          // success callback and resolve the promise          finished = true;          res();        });      }).then(() => {        // async will wait until promise.then is called        // if __zone_symbol__supportWaitUnResolvedChainedPromise is set        expect(finished).toBe(true);      });    }));  });});

Em alguns casos, você não quer acionar a nova macro task ao fazer tick. Você pode usar tick(millis, {processNewMacroTasksSynchronously: false}) para não invocar uma nova macro task.

import {fakeAsync, tick, waitForAsync} from '@angular/core/testing';import {interval, of} from 'rxjs';import {delay, take} from 'rxjs/operators';describe('Angular async helper', () => {  describe('async', () => {    let actuallyDone = false;    beforeEach(() => {      actuallyDone = false;    });    afterEach(() => {      expect(actuallyDone).withContext('actuallyDone should be true').toBe(true);    });    it('should run normal test', () => {      actuallyDone = true;    });    it('should run normal async test', (done: DoneFn) => {      setTimeout(() => {        actuallyDone = true;        done();      }, 0);    });    it('should run async test with task', waitForAsync(() => {      setTimeout(() => {        actuallyDone = true;      }, 0);    }));    it('should run async test with task', waitForAsync(() => {      const id = setInterval(() => {        actuallyDone = true;        clearInterval(id);      }, 100);    }));    it('should run async test with successful promise', waitForAsync(() => {      const p = new Promise((resolve) => {        setTimeout(resolve, 10);      });      p.then(() => {        actuallyDone = true;      });    }));    it('should run async test with failed promise', waitForAsync(() => {      const p = new Promise((resolve, reject) => {        setTimeout(reject, 10);      });      p.catch(() => {        actuallyDone = true;      });    }));    // Use done. Can also use async or fakeAsync.    it('should run async test with successful delayed Observable', (done: DoneFn) => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),        complete: done,      });    });    it('should run async test with successful delayed Observable', waitForAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });    }));    it('should run async test with successful delayed Observable', fakeAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });      tick(10);    }));  });  describe('fakeAsync', () => {    it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {      let called = false;      setTimeout(() => {        called = true;      }, 100);      tick(100);      expect(called).toBe(true);    }));    it('should run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0);      // the nested timeout will also be triggered      expect(callback).toHaveBeenCalled();    }));    it('should not run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0, {processNewMacroTasksSynchronously: false});      // the nested timeout will not be triggered      expect(callback).not.toHaveBeenCalled();      tick(0);      expect(callback).toHaveBeenCalled();    }));    it('should get Date diff correctly in fakeAsync', fakeAsync(() => {      const start = Date.now();      tick(100);      const end = Date.now();      expect(end - start).toBe(100);    }));    it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {      // need to add `import 'zone.js/plugins/zone-patch-rxjs-fake-async'      // to patch rxjs scheduler      let result = '';      of('hello')        .pipe(delay(1000))        .subscribe((v) => {          result = v;        });      expect(result).toBe('');      tick(1000);      expect(result).toBe('hello');      const start = new Date().getTime();      let dateDiff = 0;      interval(1000)        .pipe(take(2))        .subscribe(() => (dateDiff = new Date().getTime() - start));      tick(1000);      expect(dateDiff).toBe(1000);      tick(1000);      expect(dateDiff).toBe(2000);    }));  });  describe('use jasmine.clock()', () => {    // need to config __zone_symbol__fakeAsyncPatchLock flag    // before loading zone.js/testing    beforeEach(() => {      jasmine.clock().install();    });    afterEach(() => {      jasmine.clock().uninstall();    });    it('should auto enter fakeAsync', () => {      // is in fakeAsync now, don't need to call fakeAsync(testFn)      let called = false;      setTimeout(() => {        called = true;      }, 100);      jasmine.clock().tick(100);      expect(called).toBe(true);    });  });  describe('test jsonp', () => {    function jsonp(url: string, callback: () => void) {      // do a jsonp call which is not zone aware    }    // need to config __zone_symbol__supportWaitUnResolvedChainedPromise flag    // before loading zone.js/testing    it('should wait until promise.then is called', waitForAsync(() => {      let finished = false;      new Promise<void>((res) => {        jsonp('localhost:8080/jsonp', () => {          // success callback and resolve the promise          finished = true;          res();        });      }).then(() => {        // async will wait until promise.then is called        // if __zone_symbol__supportWaitUnResolvedChainedPromise is set        expect(finished).toBe(true);      });    }));  });});

Comparando datas dentro de fakeAsync()

fakeAsync() simula a passagem do tempo, o que permite calcular a diferença entre datas dentro de fakeAsync().

import {fakeAsync, tick, waitForAsync} from '@angular/core/testing';import {interval, of} from 'rxjs';import {delay, take} from 'rxjs/operators';describe('Angular async helper', () => {  describe('async', () => {    let actuallyDone = false;    beforeEach(() => {      actuallyDone = false;    });    afterEach(() => {      expect(actuallyDone).withContext('actuallyDone should be true').toBe(true);    });    it('should run normal test', () => {      actuallyDone = true;    });    it('should run normal async test', (done: DoneFn) => {      setTimeout(() => {        actuallyDone = true;        done();      }, 0);    });    it('should run async test with task', waitForAsync(() => {      setTimeout(() => {        actuallyDone = true;      }, 0);    }));    it('should run async test with task', waitForAsync(() => {      const id = setInterval(() => {        actuallyDone = true;        clearInterval(id);      }, 100);    }));    it('should run async test with successful promise', waitForAsync(() => {      const p = new Promise((resolve) => {        setTimeout(resolve, 10);      });      p.then(() => {        actuallyDone = true;      });    }));    it('should run async test with failed promise', waitForAsync(() => {      const p = new Promise((resolve, reject) => {        setTimeout(reject, 10);      });      p.catch(() => {        actuallyDone = true;      });    }));    // Use done. Can also use async or fakeAsync.    it('should run async test with successful delayed Observable', (done: DoneFn) => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),        complete: done,      });    });    it('should run async test with successful delayed Observable', waitForAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });    }));    it('should run async test with successful delayed Observable', fakeAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });      tick(10);    }));  });  describe('fakeAsync', () => {    it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {      let called = false;      setTimeout(() => {        called = true;      }, 100);      tick(100);      expect(called).toBe(true);    }));    it('should run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0);      // the nested timeout will also be triggered      expect(callback).toHaveBeenCalled();    }));    it('should not run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0, {processNewMacroTasksSynchronously: false});      // the nested timeout will not be triggered      expect(callback).not.toHaveBeenCalled();      tick(0);      expect(callback).toHaveBeenCalled();    }));    it('should get Date diff correctly in fakeAsync', fakeAsync(() => {      const start = Date.now();      tick(100);      const end = Date.now();      expect(end - start).toBe(100);    }));    it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {      // need to add `import 'zone.js/plugins/zone-patch-rxjs-fake-async'      // to patch rxjs scheduler      let result = '';      of('hello')        .pipe(delay(1000))        .subscribe((v) => {          result = v;        });      expect(result).toBe('');      tick(1000);      expect(result).toBe('hello');      const start = new Date().getTime();      let dateDiff = 0;      interval(1000)        .pipe(take(2))        .subscribe(() => (dateDiff = new Date().getTime() - start));      tick(1000);      expect(dateDiff).toBe(1000);      tick(1000);      expect(dateDiff).toBe(2000);    }));  });  describe('use jasmine.clock()', () => {    // need to config __zone_symbol__fakeAsyncPatchLock flag    // before loading zone.js/testing    beforeEach(() => {      jasmine.clock().install();    });    afterEach(() => {      jasmine.clock().uninstall();    });    it('should auto enter fakeAsync', () => {      // is in fakeAsync now, don't need to call fakeAsync(testFn)      let called = false;      setTimeout(() => {        called = true;      }, 100);      jasmine.clock().tick(100);      expect(called).toBe(true);    });  });  describe('test jsonp', () => {    function jsonp(url: string, callback: () => void) {      // do a jsonp call which is not zone aware    }    // need to config __zone_symbol__supportWaitUnResolvedChainedPromise flag    // before loading zone.js/testing    it('should wait until promise.then is called', waitForAsync(() => {      let finished = false;      new Promise<void>((res) => {        jsonp('localhost:8080/jsonp', () => {          // success callback and resolve the promise          finished = true;          res();        });      }).then(() => {        // async will wait until promise.then is called        // if __zone_symbol__supportWaitUnResolvedChainedPromise is set        expect(finished).toBe(true);      });    }));  });});

jasmine.clock com fakeAsync()

O Jasmine também fornece um recurso clock para simular datas. O Angular executa automaticamente testes que são executados após jasmine.clock().install() ser chamado dentro de um método fakeAsync() até que jasmine.clock().uninstall() seja chamado. fakeAsync() não é necessário e lança um erro se aninhado.

Por padrão, este recurso está desabilitado. Para habilitá-lo, defina uma flag global antes de importar zone-testing.

Se você usa a Angular CLI, configure esta flag em src/test.ts.

[window as any]('__zone_symbol__fakeAsyncPatchLock') = true;import 'zone.js/testing';
import {fakeAsync, tick, waitForAsync} from '@angular/core/testing';import {interval, of} from 'rxjs';import {delay, take} from 'rxjs/operators';describe('Angular async helper', () => {  describe('async', () => {    let actuallyDone = false;    beforeEach(() => {      actuallyDone = false;    });    afterEach(() => {      expect(actuallyDone).withContext('actuallyDone should be true').toBe(true);    });    it('should run normal test', () => {      actuallyDone = true;    });    it('should run normal async test', (done: DoneFn) => {      setTimeout(() => {        actuallyDone = true;        done();      }, 0);    });    it('should run async test with task', waitForAsync(() => {      setTimeout(() => {        actuallyDone = true;      }, 0);    }));    it('should run async test with task', waitForAsync(() => {      const id = setInterval(() => {        actuallyDone = true;        clearInterval(id);      }, 100);    }));    it('should run async test with successful promise', waitForAsync(() => {      const p = new Promise((resolve) => {        setTimeout(resolve, 10);      });      p.then(() => {        actuallyDone = true;      });    }));    it('should run async test with failed promise', waitForAsync(() => {      const p = new Promise((resolve, reject) => {        setTimeout(reject, 10);      });      p.catch(() => {        actuallyDone = true;      });    }));    // Use done. Can also use async or fakeAsync.    it('should run async test with successful delayed Observable', (done: DoneFn) => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),        complete: done,      });    });    it('should run async test with successful delayed Observable', waitForAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });    }));    it('should run async test with successful delayed Observable', fakeAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });      tick(10);    }));  });  describe('fakeAsync', () => {    it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {      let called = false;      setTimeout(() => {        called = true;      }, 100);      tick(100);      expect(called).toBe(true);    }));    it('should run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0);      // the nested timeout will also be triggered      expect(callback).toHaveBeenCalled();    }));    it('should not run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0, {processNewMacroTasksSynchronously: false});      // the nested timeout will not be triggered      expect(callback).not.toHaveBeenCalled();      tick(0);      expect(callback).toHaveBeenCalled();    }));    it('should get Date diff correctly in fakeAsync', fakeAsync(() => {      const start = Date.now();      tick(100);      const end = Date.now();      expect(end - start).toBe(100);    }));    it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {      // need to add `import 'zone.js/plugins/zone-patch-rxjs-fake-async'      // to patch rxjs scheduler      let result = '';      of('hello')        .pipe(delay(1000))        .subscribe((v) => {          result = v;        });      expect(result).toBe('');      tick(1000);      expect(result).toBe('hello');      const start = new Date().getTime();      let dateDiff = 0;      interval(1000)        .pipe(take(2))        .subscribe(() => (dateDiff = new Date().getTime() - start));      tick(1000);      expect(dateDiff).toBe(1000);      tick(1000);      expect(dateDiff).toBe(2000);    }));  });  describe('use jasmine.clock()', () => {    // need to config __zone_symbol__fakeAsyncPatchLock flag    // before loading zone.js/testing    beforeEach(() => {      jasmine.clock().install();    });    afterEach(() => {      jasmine.clock().uninstall();    });    it('should auto enter fakeAsync', () => {      // is in fakeAsync now, don't need to call fakeAsync(testFn)      let called = false;      setTimeout(() => {        called = true;      }, 100);      jasmine.clock().tick(100);      expect(called).toBe(true);    });  });  describe('test jsonp', () => {    function jsonp(url: string, callback: () => void) {      // do a jsonp call which is not zone aware    }    // need to config __zone_symbol__supportWaitUnResolvedChainedPromise flag    // before loading zone.js/testing    it('should wait until promise.then is called', waitForAsync(() => {      let finished = false;      new Promise<void>((res) => {        jsonp('localhost:8080/jsonp', () => {          // success callback and resolve the promise          finished = true;          res();        });      }).then(() => {        // async will wait until promise.then is called        // if __zone_symbol__supportWaitUnResolvedChainedPromise is set        expect(finished).toBe(true);      });    }));  });});

Usando o scheduler RxJS dentro de fakeAsync()

Você também pode usar o scheduler RxJS em fakeAsync() assim como usar setTimeout() ou setInterval(), mas você precisa importar zone.js/plugins/zone-patch-rxjs-fake-async para aplicar patch no scheduler RxJS.

import {fakeAsync, tick, waitForAsync} from '@angular/core/testing';import {interval, of} from 'rxjs';import {delay, take} from 'rxjs/operators';describe('Angular async helper', () => {  describe('async', () => {    let actuallyDone = false;    beforeEach(() => {      actuallyDone = false;    });    afterEach(() => {      expect(actuallyDone).withContext('actuallyDone should be true').toBe(true);    });    it('should run normal test', () => {      actuallyDone = true;    });    it('should run normal async test', (done: DoneFn) => {      setTimeout(() => {        actuallyDone = true;        done();      }, 0);    });    it('should run async test with task', waitForAsync(() => {      setTimeout(() => {        actuallyDone = true;      }, 0);    }));    it('should run async test with task', waitForAsync(() => {      const id = setInterval(() => {        actuallyDone = true;        clearInterval(id);      }, 100);    }));    it('should run async test with successful promise', waitForAsync(() => {      const p = new Promise((resolve) => {        setTimeout(resolve, 10);      });      p.then(() => {        actuallyDone = true;      });    }));    it('should run async test with failed promise', waitForAsync(() => {      const p = new Promise((resolve, reject) => {        setTimeout(reject, 10);      });      p.catch(() => {        actuallyDone = true;      });    }));    // Use done. Can also use async or fakeAsync.    it('should run async test with successful delayed Observable', (done: DoneFn) => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),        complete: done,      });    });    it('should run async test with successful delayed Observable', waitForAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });    }));    it('should run async test with successful delayed Observable', fakeAsync(() => {      const source = of(true).pipe(delay(10));      source.subscribe({        next: (val) => (actuallyDone = true),        error: (err) => fail(err),      });      tick(10);    }));  });  describe('fakeAsync', () => {    it('should run timeout callback with delay after call tick with millis', fakeAsync(() => {      let called = false;      setTimeout(() => {        called = true;      }, 100);      tick(100);      expect(called).toBe(true);    }));    it('should run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0);      // the nested timeout will also be triggered      expect(callback).toHaveBeenCalled();    }));    it('should not run new macro task callback with delay after call tick with millis', fakeAsync(() => {      function nestedTimer(cb: () => any): void {        setTimeout(() => setTimeout(() => cb()));      }      const callback = jasmine.createSpy('callback');      nestedTimer(callback);      expect(callback).not.toHaveBeenCalled();      tick(0, {processNewMacroTasksSynchronously: false});      // the nested timeout will not be triggered      expect(callback).not.toHaveBeenCalled();      tick(0);      expect(callback).toHaveBeenCalled();    }));    it('should get Date diff correctly in fakeAsync', fakeAsync(() => {      const start = Date.now();      tick(100);      const end = Date.now();      expect(end - start).toBe(100);    }));    it('should get Date diff correctly in fakeAsync with rxjs scheduler', fakeAsync(() => {      // need to add `import 'zone.js/plugins/zone-patch-rxjs-fake-async'      // to patch rxjs scheduler      let result = '';      of('hello')        .pipe(delay(1000))        .subscribe((v) => {          result = v;        });      expect(result).toBe('');      tick(1000);      expect(result).toBe('hello');      const start = new Date().getTime();      let dateDiff = 0;      interval(1000)        .pipe(take(2))        .subscribe(() => (dateDiff = new Date().getTime() - start));      tick(1000);      expect(dateDiff).toBe(1000);      tick(1000);      expect(dateDiff).toBe(2000);    }));  });  describe('use jasmine.clock()', () => {    // need to config __zone_symbol__fakeAsyncPatchLock flag    // before loading zone.js/testing    beforeEach(() => {      jasmine.clock().install();    });    afterEach(() => {      jasmine.clock().uninstall();    });    it('should auto enter fakeAsync', () => {      // is in fakeAsync now, don't need to call fakeAsync(testFn)      let called = false;      setTimeout(() => {        called = true;      }, 100);      jasmine.clock().tick(100);      expect(called).toBe(true);    });  });  describe('test jsonp', () => {    function jsonp(url: string, callback: () => void) {      // do a jsonp call which is not zone aware    }    // need to config __zone_symbol__supportWaitUnResolvedChainedPromise flag    // before loading zone.js/testing    it('should wait until promise.then is called', waitForAsync(() => {      let finished = false;      new Promise<void>((res) => {        jsonp('localhost:8080/jsonp', () => {          // success callback and resolve the promise          finished = true;          res();        });      }).then(() => {        // async will wait until promise.then is called        // if __zone_symbol__supportWaitUnResolvedChainedPromise is set        expect(finished).toBe(true);      });    }));  });});

Suportar mais macroTasks

Por padrão, fakeAsync() suporta as seguintes macro tasks.

  • setTimeout
  • setInterval
  • requestAnimationFrame
  • webkitRequestAnimationFrame
  • mozRequestAnimationFrame

Se você executar outras macro tasks como HTMLCanvasElement.toBlob(), um erro "Unknown macroTask scheduled in fake async test" é lançado.

src/app/shared/canvas.component.spec.ts (failing)

import {fakeAsync, TestBed, tick} from '@angular/core/testing';import {CanvasComponent} from './canvas.component';describe('CanvasComponent', () => {  beforeEach(() => {    (window as any).__zone_symbol__FakeAsyncTestMacroTask = [      {        source: 'HTMLCanvasElement.toBlob',        callbackArgs: [{size: 200}],      },    ];  });  it('should be able to generate blob data from canvas', fakeAsync(() => {    const fixture = TestBed.createComponent(CanvasComponent);    const canvasComp = fixture.componentInstance;    fixture.detectChanges();    expect(canvasComp.blobSize).toBe(0);    tick();    expect(canvasComp.blobSize).toBeGreaterThan(0);  }));});

src/app/shared/canvas.component.ts

// Import patch to make async `HTMLCanvasElement` methods (such as `.toBlob()`) Zone.js-aware.// Either import in `polyfills.ts` (if used in more than one places in the app) or in the component// file using `HTMLCanvasElement` (if it is only used in a single file).import 'zone.js/plugins/zone-patch-canvas';import {Component, AfterViewInit, ViewChild, ElementRef} from '@angular/core';@Component({  selector: 'sample-canvas',  template: '<canvas #sampleCanvas width="200" height="200"></canvas>',})export class CanvasComponent implements AfterViewInit {  blobSize = 0;  @ViewChild('sampleCanvas') sampleCanvas!: ElementRef;  ngAfterViewInit() {    const canvas: HTMLCanvasElement = this.sampleCanvas.nativeElement;    const context = canvas.getContext('2d')!;    context.clearRect(0, 0, 200, 200);    context.fillStyle = '#FF1122';    context.fillRect(0, 0, 200, 200);    canvas.toBlob((blob) => {      this.blobSize = blob?.size ?? 0;    });  }}

Se você quiser suportar tal caso, você precisa definir a macro task que quer suportar em beforeEach(). Por exemplo:

src/app/shared/canvas.component.spec.ts (excerpt)

import {fakeAsync, TestBed, tick} from '@angular/core/testing';import {CanvasComponent} from './canvas.component';describe('CanvasComponent', () => {  beforeEach(() => {    (window as any).__zone_symbol__FakeAsyncTestMacroTask = [      {        source: 'HTMLCanvasElement.toBlob',        callbackArgs: [{size: 200}],      },    ];  });  it('should be able to generate blob data from canvas', fakeAsync(() => {    const fixture = TestBed.createComponent(CanvasComponent);    const canvasComp = fixture.componentInstance;    fixture.detectChanges();    expect(canvasComp.blobSize).toBe(0);    tick();    expect(canvasComp.blobSize).toBeGreaterThan(0);  }));});

ÚTIL: Para tornar o elemento <canvas> ciente do Zone.js em sua aplicação, você precisa importar o patch zone-patch-canvas (seja em polyfills.ts ou no arquivo específico que usa <canvas>):

src/polyfills.ts or src/app/shared/canvas.component.ts

// Import patch to make async `HTMLCanvasElement` methods (such as `.toBlob()`) Zone.js-aware.// Either import in `polyfills.ts` (if used in more than one places in the app) or in the component// file using `HTMLCanvasElement` (if it is only used in a single file).import 'zone.js/plugins/zone-patch-canvas';import {Component, AfterViewInit, ViewChild, ElementRef} from '@angular/core';@Component({  selector: 'sample-canvas',  template: '<canvas #sampleCanvas width="200" height="200"></canvas>',})export class CanvasComponent implements AfterViewInit {  blobSize = 0;  @ViewChild('sampleCanvas') sampleCanvas!: ElementRef;  ngAfterViewInit() {    const canvas: HTMLCanvasElement = this.sampleCanvas.nativeElement;    const context = canvas.getContext('2d')!;    context.clearRect(0, 0, 200, 200);    context.fillStyle = '#FF1122';    context.fillRect(0, 0, 200, 200);    canvas.toBlob((blob) => {      this.blobSize = blob?.size ?? 0;    });  }}

Observables assíncronos

Você pode estar satisfeito com a cobertura de testes desses testes.

No entanto, você pode estar preocupado com o fato de que o service real não se comporta exatamente assim. O service real envia requisições a um servidor remoto. Um servidor leva tempo para responder e a resposta certamente não estará disponível imediatamente como nos dois testes anteriores.

Seus testes refletirão o mundo real com mais fidelidade se você retornar um observable assíncrono do spy getQuote() assim.

import {fakeAsync, ComponentFixture, TestBed, tick, waitForAsync} from '@angular/core/testing';import {asyncData, asyncError} from '../../testing';import {Subject, defer, of, throwError} from 'rxjs';import {last} from 'rxjs/operators';import {TwainComponent} from './twain.component';import {TwainService} from './twain.service';describe('TwainComponent', () => {  let component: TwainComponent;  let fixture: ComponentFixture<TwainComponent>;  let getQuoteSpy: jasmine.Spy;  let quoteEl: HTMLElement;  let testQuote: string;  // Helper function to get the error message element value  // An *ngIf keeps it out of the DOM until there is an error  const errorMessage = () => {    const el = fixture.nativeElement.querySelector('.error');    return el ? el.textContent : null;  };  beforeEach(() => {    TestBed.configureTestingModule({      providers: [TwainService],    });    testQuote = 'Test Quote';    // Create a fake TwainService object with a `getQuote()` spy    const twainService = TestBed.inject(TwainService);    // Make the spy return a synchronous Observable with the test data    getQuoteSpy = spyOn(twainService, 'getQuote').and.returnValue(of(testQuote));    fixture = TestBed.createComponent(TwainComponent);    fixture.autoDetectChanges();    component = fixture.componentInstance;    quoteEl = fixture.nativeElement.querySelector('.twain');  });  describe('when test with synchronous observable', () => {    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    // The quote would not be immediately available if the service were truly async.    it('should show quote after component initialized', async () => {      await fixture.whenStable(); // onInit()      // sync spy result shows testQuote immediately after init      expect(quoteEl.textContent).toBe(testQuote);      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    // The error would not be immediately available if the service were truly async.    // Use `fakeAsync` because the component error calls `setTimeout`    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an error observable after a timeout      getQuoteSpy.and.returnValue(        defer(() => {          return new Promise((resolve, reject) => {            setTimeout(() => {              reject('TwainService test failure');            });          });        }),      );      fixture.detectChanges(); // onInit()      // sync spy errors immediately after init      tick(); // flush the setTimeout()      fixture.detectChanges(); // update errorMessage within setTimeout()      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });  describe('when test with asynchronous observable', () => {    beforeEach(() => {      // Simulate delayed observable values with the `asyncData()` helper      getQuoteSpy.and.returnValue(asyncData(testQuote));    });    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    it('should still not show quote after component initialized', () => {      fixture.detectChanges();      // getQuote service is async => still has not returned with quote      // so should show the start value, '...'      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      expect(errorMessage()).withContext('should not show error').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      tick(); // flush the observable to get the quote      fixture.detectChanges(); // update view      expect(quoteEl.textContent).withContext('should show quote').toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    }));    it('should show quote after getQuote (async)', async () => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      await fixture.whenStable();      // wait for async getQuote      fixture.detectChanges(); // update view with quote      expect(quoteEl.textContent).toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    });    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an async error observable      getQuoteSpy.and.returnValue(asyncError<string>('TwainService test failure'));      fixture.detectChanges();      tick(); // component shows error after a setTimeout()      fixture.detectChanges(); // update error message      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });});

Helpers de observable assíncrono

O observable assíncrono foi produzido por um helper asyncData. O helper asyncData é uma função utilitária que você terá que escrever você mesmo, ou copiar esta do código de exemplo.

testing/async-observable-helpers.ts

/* * Mock async observables that return asynchronously. * The observable either emits once and completes or errors. * * Must call `tick()` when test with `fakeAsync()`. * * THE FOLLOWING DON'T WORK * Using `of().delay()` triggers TestBed errors; * see https://github.com/angular/angular/issues/10127 . * * Using `asap` scheduler - as in `of(value, asap)` - doesn't work either. */import {defer} from 'rxjs';/** * Create async observable that emits-once and completes * after a JS engine turn */export function asyncData<T>(data: T) {  return defer(() => Promise.resolve(data));}/** * Create async observable error that errors * after a JS engine turn */export function asyncError<T>(errorObject: any) {  return defer(() => Promise.reject(errorObject));}

Este observable do helper emite o valor data no próximo turno do motor JavaScript.

O operador defer() do RxJS retorna um observable. Ele recebe uma função factory que retorna uma promise ou um observable. Quando algo se inscreve no observable do defer, ele adiciona o subscriber a um novo observable criado com aquela factory.

O operador defer() transforma o Promise.resolve() em um novo observable que, como HttpClient, emite uma vez e completa. Subscribers são desincritos após receberem o valor de dados.

Há um helper similar para produzir um erro assíncrono.

/* * Mock async observables that return asynchronously. * The observable either emits once and completes or errors. * * Must call `tick()` when test with `fakeAsync()`. * * THE FOLLOWING DON'T WORK * Using `of().delay()` triggers TestBed errors; * see https://github.com/angular/angular/issues/10127 . * * Using `asap` scheduler - as in `of(value, asap)` - doesn't work either. */import {defer} from 'rxjs';/** * Create async observable that emits-once and completes * after a JS engine turn */export function asyncData<T>(data: T) {  return defer(() => Promise.resolve(data));}/** * Create async observable error that errors * after a JS engine turn */export function asyncError<T>(errorObject: any) {  return defer(() => Promise.reject(errorObject));}

Mais testes assíncronos

Agora que o spy getQuote() está retornando observables assíncronos, a maioria de seus testes terá que ser assíncrona também.

Aqui está um teste fakeAsync() que demonstra o fluxo de dados que você esperaria no mundo real.

import {fakeAsync, ComponentFixture, TestBed, tick, waitForAsync} from '@angular/core/testing';import {asyncData, asyncError} from '../../testing';import {Subject, defer, of, throwError} from 'rxjs';import {last} from 'rxjs/operators';import {TwainComponent} from './twain.component';import {TwainService} from './twain.service';describe('TwainComponent', () => {  let component: TwainComponent;  let fixture: ComponentFixture<TwainComponent>;  let getQuoteSpy: jasmine.Spy;  let quoteEl: HTMLElement;  let testQuote: string;  // Helper function to get the error message element value  // An *ngIf keeps it out of the DOM until there is an error  const errorMessage = () => {    const el = fixture.nativeElement.querySelector('.error');    return el ? el.textContent : null;  };  beforeEach(() => {    TestBed.configureTestingModule({      providers: [TwainService],    });    testQuote = 'Test Quote';    // Create a fake TwainService object with a `getQuote()` spy    const twainService = TestBed.inject(TwainService);    // Make the spy return a synchronous Observable with the test data    getQuoteSpy = spyOn(twainService, 'getQuote').and.returnValue(of(testQuote));    fixture = TestBed.createComponent(TwainComponent);    fixture.autoDetectChanges();    component = fixture.componentInstance;    quoteEl = fixture.nativeElement.querySelector('.twain');  });  describe('when test with synchronous observable', () => {    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    // The quote would not be immediately available if the service were truly async.    it('should show quote after component initialized', async () => {      await fixture.whenStable(); // onInit()      // sync spy result shows testQuote immediately after init      expect(quoteEl.textContent).toBe(testQuote);      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    // The error would not be immediately available if the service were truly async.    // Use `fakeAsync` because the component error calls `setTimeout`    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an error observable after a timeout      getQuoteSpy.and.returnValue(        defer(() => {          return new Promise((resolve, reject) => {            setTimeout(() => {              reject('TwainService test failure');            });          });        }),      );      fixture.detectChanges(); // onInit()      // sync spy errors immediately after init      tick(); // flush the setTimeout()      fixture.detectChanges(); // update errorMessage within setTimeout()      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });  describe('when test with asynchronous observable', () => {    beforeEach(() => {      // Simulate delayed observable values with the `asyncData()` helper      getQuoteSpy.and.returnValue(asyncData(testQuote));    });    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    it('should still not show quote after component initialized', () => {      fixture.detectChanges();      // getQuote service is async => still has not returned with quote      // so should show the start value, '...'      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      expect(errorMessage()).withContext('should not show error').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      tick(); // flush the observable to get the quote      fixture.detectChanges(); // update view      expect(quoteEl.textContent).withContext('should show quote').toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    }));    it('should show quote after getQuote (async)', async () => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      await fixture.whenStable();      // wait for async getQuote      fixture.detectChanges(); // update view with quote      expect(quoteEl.textContent).toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    });    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an async error observable      getQuoteSpy.and.returnValue(asyncError<string>('TwainService test failure'));      fixture.detectChanges();      tick(); // component shows error after a setTimeout()      fixture.detectChanges(); // update error message      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });});

Observe que o elemento de citação exibe o valor placeholder ('...') após ngOnInit(). A primeira citação ainda não chegou.

Para liberar a primeira citação do observable, você chama tick(). Então chame detectChanges() para dizer ao Angular para atualizar a tela.

Então você pode afirmar que o elemento de citação exibe o texto esperado.

Teste assíncrono sem fakeAsync()

Aqui está o teste fakeAsync() anterior, reescrito com async.

import {fakeAsync, ComponentFixture, TestBed, tick, waitForAsync} from '@angular/core/testing';import {asyncData, asyncError} from '../../testing';import {Subject, defer, of, throwError} from 'rxjs';import {last} from 'rxjs/operators';import {TwainComponent} from './twain.component';import {TwainService} from './twain.service';describe('TwainComponent', () => {  let component: TwainComponent;  let fixture: ComponentFixture<TwainComponent>;  let getQuoteSpy: jasmine.Spy;  let quoteEl: HTMLElement;  let testQuote: string;  // Helper function to get the error message element value  // An *ngIf keeps it out of the DOM until there is an error  const errorMessage = () => {    const el = fixture.nativeElement.querySelector('.error');    return el ? el.textContent : null;  };  beforeEach(() => {    TestBed.configureTestingModule({      providers: [TwainService],    });    testQuote = 'Test Quote';    // Create a fake TwainService object with a `getQuote()` spy    const twainService = TestBed.inject(TwainService);    // Make the spy return a synchronous Observable with the test data    getQuoteSpy = spyOn(twainService, 'getQuote').and.returnValue(of(testQuote));    fixture = TestBed.createComponent(TwainComponent);    fixture.autoDetectChanges();    component = fixture.componentInstance;    quoteEl = fixture.nativeElement.querySelector('.twain');  });  describe('when test with synchronous observable', () => {    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    // The quote would not be immediately available if the service were truly async.    it('should show quote after component initialized', async () => {      await fixture.whenStable(); // onInit()      // sync spy result shows testQuote immediately after init      expect(quoteEl.textContent).toBe(testQuote);      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    // The error would not be immediately available if the service were truly async.    // Use `fakeAsync` because the component error calls `setTimeout`    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an error observable after a timeout      getQuoteSpy.and.returnValue(        defer(() => {          return new Promise((resolve, reject) => {            setTimeout(() => {              reject('TwainService test failure');            });          });        }),      );      fixture.detectChanges(); // onInit()      // sync spy errors immediately after init      tick(); // flush the setTimeout()      fixture.detectChanges(); // update errorMessage within setTimeout()      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });  describe('when test with asynchronous observable', () => {    beforeEach(() => {      // Simulate delayed observable values with the `asyncData()` helper      getQuoteSpy.and.returnValue(asyncData(testQuote));    });    it('should not show quote before OnInit', () => {      expect(quoteEl.textContent).withContext('nothing displayed').toBe('');      expect(errorMessage()).withContext('should not show error element').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote not yet called').toBe(false);    });    it('should still not show quote after component initialized', () => {      fixture.detectChanges();      // getQuote service is async => still has not returned with quote      // so should show the start value, '...'      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      expect(errorMessage()).withContext('should not show error').toBeNull();      expect(getQuoteSpy.calls.any()).withContext('getQuote called').toBe(true);    });    it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      tick(); // flush the observable to get the quote      fixture.detectChanges(); // update view      expect(quoteEl.textContent).withContext('should show quote').toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    }));    it('should show quote after getQuote (async)', async () => {      fixture.detectChanges(); // ngOnInit()      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');      await fixture.whenStable();      // wait for async getQuote      fixture.detectChanges(); // update view with quote      expect(quoteEl.textContent).toBe(testQuote);      expect(errorMessage()).withContext('should not show error').toBeNull();    });    it('should display error when TwainService fails', fakeAsync(() => {      // tell spy to return an async error observable      getQuoteSpy.and.returnValue(asyncError<string>('TwainService test failure'));      fixture.detectChanges();      tick(); // component shows error after a setTimeout()      fixture.detectChanges(); // update error message      expect(errorMessage())        .withContext('should display error')        .toMatch(/test failure/);      expect(quoteEl.textContent).withContext('should show placeholder').toBe('...');    }));  });});

whenStable

O teste deve esperar que o observable getQuote() emita a próxima citação. Em vez de chamar tick(), ele chama fixture.whenStable().

O fixture.whenStable() retorna uma promise que resolve quando a fila de tarefas do motor JavaScript fica vazia. Neste exemplo, a fila de tarefas fica vazia quando o observable emite a primeira citação.

Component com inputs e outputs

Um component com inputs e outputs normalmente aparece dentro do template de view de um component host. O host usa um property binding para definir a propriedade input e um event binding para ouvir eventos levantados pela propriedade output.

O objetivo do teste é verificar que tais bindings funcionam conforme esperado. Os testes devem definir valores input e ouvir eventos output.

O DashboardHeroComponent é um pequeno exemplo de um component neste papel. Ele exibe um hero individual fornecido pelo DashboardComponent. Clicar naquele hero diz ao DashboardComponent que o usuário selecionou o hero.

O DashboardHeroComponent é incorporado no template DashboardComponent assim:

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

<h2 highlight>{{ title }}</h2><div class="grid grid-pad">  @for (hero of heroes; track hero) {    <dashboard-hero      class="col-1-4"      [hero]="hero"      (selected)="gotoDetail($event)"    >    </dashboard-hero>  }</div>

O DashboardHeroComponent aparece em um bloco @for, que define a propriedade input hero de cada component para o valor em loop e ouve o evento selected do component.

Aqui está a definição completa do component:

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

import {Component, input, output} from '@angular/core';import {UpperCasePipe} from '@angular/common';import {Hero} from '../model/hero';@Component({  selector: 'dashboard-hero',  template: `    <button type="button" (click)="click()" class="hero">      {{ hero().name | uppercase }}    </button>  `,  styleUrls: ['./dashboard-hero.component.css'],  imports: [UpperCasePipe],})export class DashboardHeroComponent {  readonly hero = input.required<Hero>();  readonly selected = output<Hero>();  click() {    this.selected.emit(this.hero());  }}

Embora testar um component tão simples tenha pouco valor intrínseco, vale a pena saber como. Use uma dessas abordagens:

  • Teste-o como usado por DashboardComponent
  • Teste-o como um component autônomo
  • Teste-o como usado por um substituto para DashboardComponent

O objetivo imediato é testar o DashboardHeroComponent, não o DashboardComponent, então tente a segunda e terceira opções.

Testar DashboardHeroComponent autônomo

Aqui está a parte principal da configuração do arquivo spec.

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

import {DebugElement} from '@angular/core';import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {first} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {appProviders} from '../app.config';import {Hero} from '../model/hero';import {DashboardHeroComponent} from './dashboard-hero.component';beforeEach(addMatchers);describe('DashboardHeroComponent when tested directly', () => {  let comp: DashboardHeroComponent;  let expectedHero: Hero;  let fixture: ComponentFixture<DashboardHeroComponent>;  let heroDe: DebugElement;  let heroEl: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  });  beforeEach(async () => {    fixture = TestBed.createComponent(DashboardHeroComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // find the hero's DebugElement and element    heroDe = fixture.debugElement.query(By.css('.hero'));    heroEl = heroDe.nativeElement;    // mock the hero supplied by the parent component    expectedHero = {id: 42, name: 'Test Name'};    // simulate the parent setting the input property with that hero    fixture.componentRef.setInput('hero', expectedHero);    // wait for initial data binding    await fixture.whenStable();  });  it('should display hero name in uppercase', () => {    const expectedPipedName = expectedHero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked (triggerEventHandler)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroDe.triggerEventHandler('click');    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (element.click)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroEl.click();    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with DebugElement)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroDe); // click helper with DebugElement    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with native element)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroEl); // click helper with native element    expect(selectedHero).toBe(expectedHero);  });});//////////////////describe('DashboardHeroComponent when inside a test host', () => {  let testHost: TestHostComponent;  let fixture: ComponentFixture<TestHostComponent>;  let heroEl: HTMLElement;  beforeEach(waitForAsync(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  }));  beforeEach(() => {    // create TestHostComponent instead of DashboardHeroComponent    fixture = TestBed.createComponent(TestHostComponent);    testHost = fixture.componentInstance;    heroEl = fixture.nativeElement.querySelector('.hero');    fixture.detectChanges(); // trigger initial data binding  });  it('should display hero name', () => {    const expectedPipedName = testHost.hero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked', () => {    click(heroEl);    // selected hero should be the same data bound hero    expect(testHost.selectedHero).toBe(testHost.hero);  });});////// Test Host Component //////import {Component} from '@angular/core';@Component({  imports: [DashboardHeroComponent],  template: ` <dashboard-hero [hero]="hero" (selected)="onSelected($event)"> </dashboard-hero>`,})class TestHostComponent {  hero: Hero = {id: 42, name: 'Test Name'};  selectedHero: Hero | undefined;  onSelected(hero: Hero) {    this.selectedHero = hero;  }}

Observe como o código de configuração atribui um hero de teste (expectedHero) à propriedade hero do component, emulando a maneira como o DashboardComponent a definiria usando o property binding em seu repetidor.

O teste seguinte verifica que o nome do hero é propagado para o template usando um binding.

import {DebugElement} from '@angular/core';import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {first} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {appProviders} from '../app.config';import {Hero} from '../model/hero';import {DashboardHeroComponent} from './dashboard-hero.component';beforeEach(addMatchers);describe('DashboardHeroComponent when tested directly', () => {  let comp: DashboardHeroComponent;  let expectedHero: Hero;  let fixture: ComponentFixture<DashboardHeroComponent>;  let heroDe: DebugElement;  let heroEl: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  });  beforeEach(async () => {    fixture = TestBed.createComponent(DashboardHeroComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // find the hero's DebugElement and element    heroDe = fixture.debugElement.query(By.css('.hero'));    heroEl = heroDe.nativeElement;    // mock the hero supplied by the parent component    expectedHero = {id: 42, name: 'Test Name'};    // simulate the parent setting the input property with that hero    fixture.componentRef.setInput('hero', expectedHero);    // wait for initial data binding    await fixture.whenStable();  });  it('should display hero name in uppercase', () => {    const expectedPipedName = expectedHero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked (triggerEventHandler)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroDe.triggerEventHandler('click');    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (element.click)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroEl.click();    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with DebugElement)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroDe); // click helper with DebugElement    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with native element)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroEl); // click helper with native element    expect(selectedHero).toBe(expectedHero);  });});//////////////////describe('DashboardHeroComponent when inside a test host', () => {  let testHost: TestHostComponent;  let fixture: ComponentFixture<TestHostComponent>;  let heroEl: HTMLElement;  beforeEach(waitForAsync(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  }));  beforeEach(() => {    // create TestHostComponent instead of DashboardHeroComponent    fixture = TestBed.createComponent(TestHostComponent);    testHost = fixture.componentInstance;    heroEl = fixture.nativeElement.querySelector('.hero');    fixture.detectChanges(); // trigger initial data binding  });  it('should display hero name', () => {    const expectedPipedName = testHost.hero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked', () => {    click(heroEl);    // selected hero should be the same data bound hero    expect(testHost.selectedHero).toBe(testHost.hero);  });});////// Test Host Component //////import {Component} from '@angular/core';@Component({  imports: [DashboardHeroComponent],  template: ` <dashboard-hero [hero]="hero" (selected)="onSelected($event)"> </dashboard-hero>`,})class TestHostComponent {  hero: Hero = {id: 42, name: 'Test Name'};  selectedHero: Hero | undefined;  onSelected(hero: Hero) {    this.selectedHero = hero;  }}

Como o template passa o nome do hero através do UpperCasePipe do Angular, o teste deve corresponder o valor do elemento com o nome em maiúsculas.

Clicar

Clicar no hero deve acionar um evento selected que o component host (presumivelmente DashboardComponent) pode ouvir:

import {DebugElement} from '@angular/core';import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {first} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {appProviders} from '../app.config';import {Hero} from '../model/hero';import {DashboardHeroComponent} from './dashboard-hero.component';beforeEach(addMatchers);describe('DashboardHeroComponent when tested directly', () => {  let comp: DashboardHeroComponent;  let expectedHero: Hero;  let fixture: ComponentFixture<DashboardHeroComponent>;  let heroDe: DebugElement;  let heroEl: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  });  beforeEach(async () => {    fixture = TestBed.createComponent(DashboardHeroComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // find the hero's DebugElement and element    heroDe = fixture.debugElement.query(By.css('.hero'));    heroEl = heroDe.nativeElement;    // mock the hero supplied by the parent component    expectedHero = {id: 42, name: 'Test Name'};    // simulate the parent setting the input property with that hero    fixture.componentRef.setInput('hero', expectedHero);    // wait for initial data binding    await fixture.whenStable();  });  it('should display hero name in uppercase', () => {    const expectedPipedName = expectedHero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked (triggerEventHandler)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroDe.triggerEventHandler('click');    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (element.click)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroEl.click();    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with DebugElement)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroDe); // click helper with DebugElement    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with native element)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroEl); // click helper with native element    expect(selectedHero).toBe(expectedHero);  });});//////////////////describe('DashboardHeroComponent when inside a test host', () => {  let testHost: TestHostComponent;  let fixture: ComponentFixture<TestHostComponent>;  let heroEl: HTMLElement;  beforeEach(waitForAsync(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  }));  beforeEach(() => {    // create TestHostComponent instead of DashboardHeroComponent    fixture = TestBed.createComponent(TestHostComponent);    testHost = fixture.componentInstance;    heroEl = fixture.nativeElement.querySelector('.hero');    fixture.detectChanges(); // trigger initial data binding  });  it('should display hero name', () => {    const expectedPipedName = testHost.hero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked', () => {    click(heroEl);    // selected hero should be the same data bound hero    expect(testHost.selectedHero).toBe(testHost.hero);  });});////// Test Host Component //////import {Component} from '@angular/core';@Component({  imports: [DashboardHeroComponent],  template: ` <dashboard-hero [hero]="hero" (selected)="onSelected($event)"> </dashboard-hero>`,})class TestHostComponent {  hero: Hero = {id: 42, name: 'Test Name'};  selectedHero: Hero | undefined;  onSelected(hero: Hero) {    this.selectedHero = hero;  }}

A propriedade selected do component retorna um EventEmitter, que parece um Observable síncrono RxJS para consumidores. O teste se inscreve nele explicitamente assim como o component host faz implicitamente.

Se o component se comporta como esperado, clicar no elemento do hero deve dizer à propriedade selected do component para emitir o objeto hero.

O teste detecta aquele evento através de sua inscrição em selected.

triggerEventHandler

O heroDe no teste anterior é um DebugElement que representa o <div> do hero.

Ele tem propriedades e métodos do Angular que abstraem a interação com o elemento nativo. Este teste chama o DebugElement.triggerEventHandler com o nome do evento "click". O binding do evento "click" responde chamando DashboardHeroComponent.click().

O DebugElement.triggerEventHandler do Angular pode acionar qualquer evento data-bound por seu nome de evento. O segundo parâmetro é o objeto de evento passado ao handler.

O teste acionou um evento "click".

import {DebugElement} from '@angular/core';import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {first} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {appProviders} from '../app.config';import {Hero} from '../model/hero';import {DashboardHeroComponent} from './dashboard-hero.component';beforeEach(addMatchers);describe('DashboardHeroComponent when tested directly', () => {  let comp: DashboardHeroComponent;  let expectedHero: Hero;  let fixture: ComponentFixture<DashboardHeroComponent>;  let heroDe: DebugElement;  let heroEl: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  });  beforeEach(async () => {    fixture = TestBed.createComponent(DashboardHeroComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // find the hero's DebugElement and element    heroDe = fixture.debugElement.query(By.css('.hero'));    heroEl = heroDe.nativeElement;    // mock the hero supplied by the parent component    expectedHero = {id: 42, name: 'Test Name'};    // simulate the parent setting the input property with that hero    fixture.componentRef.setInput('hero', expectedHero);    // wait for initial data binding    await fixture.whenStable();  });  it('should display hero name in uppercase', () => {    const expectedPipedName = expectedHero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked (triggerEventHandler)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroDe.triggerEventHandler('click');    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (element.click)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroEl.click();    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with DebugElement)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroDe); // click helper with DebugElement    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with native element)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroEl); // click helper with native element    expect(selectedHero).toBe(expectedHero);  });});//////////////////describe('DashboardHeroComponent when inside a test host', () => {  let testHost: TestHostComponent;  let fixture: ComponentFixture<TestHostComponent>;  let heroEl: HTMLElement;  beforeEach(waitForAsync(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  }));  beforeEach(() => {    // create TestHostComponent instead of DashboardHeroComponent    fixture = TestBed.createComponent(TestHostComponent);    testHost = fixture.componentInstance;    heroEl = fixture.nativeElement.querySelector('.hero');    fixture.detectChanges(); // trigger initial data binding  });  it('should display hero name', () => {    const expectedPipedName = testHost.hero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked', () => {    click(heroEl);    // selected hero should be the same data bound hero    expect(testHost.selectedHero).toBe(testHost.hero);  });});////// Test Host Component //////import {Component} from '@angular/core';@Component({  imports: [DashboardHeroComponent],  template: ` <dashboard-hero [hero]="hero" (selected)="onSelected($event)"> </dashboard-hero>`,})class TestHostComponent {  hero: Hero = {id: 42, name: 'Test Name'};  selectedHero: Hero | undefined;  onSelected(hero: Hero) {    this.selectedHero = hero;  }}

Neste caso, o teste assume corretamente que o handler de evento runtime, o método click() do component, não se importa com o objeto de evento.

ÚTIL: Outros handlers são menos tolerantes. Por exemplo, a directive RouterLink espera um objeto com uma propriedade button que identifica qual botão do mouse, se houver, foi pressionado durante o clique. A directive RouterLink lança um erro se o objeto de evento estiver faltando.

Clicar no elemento

A seguinte alternativa de teste chama o método click() próprio do elemento nativo, o que é perfeitamente adequado para este component.

import {DebugElement} from '@angular/core';import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {first} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {appProviders} from '../app.config';import {Hero} from '../model/hero';import {DashboardHeroComponent} from './dashboard-hero.component';beforeEach(addMatchers);describe('DashboardHeroComponent when tested directly', () => {  let comp: DashboardHeroComponent;  let expectedHero: Hero;  let fixture: ComponentFixture<DashboardHeroComponent>;  let heroDe: DebugElement;  let heroEl: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  });  beforeEach(async () => {    fixture = TestBed.createComponent(DashboardHeroComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // find the hero's DebugElement and element    heroDe = fixture.debugElement.query(By.css('.hero'));    heroEl = heroDe.nativeElement;    // mock the hero supplied by the parent component    expectedHero = {id: 42, name: 'Test Name'};    // simulate the parent setting the input property with that hero    fixture.componentRef.setInput('hero', expectedHero);    // wait for initial data binding    await fixture.whenStable();  });  it('should display hero name in uppercase', () => {    const expectedPipedName = expectedHero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked (triggerEventHandler)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroDe.triggerEventHandler('click');    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (element.click)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroEl.click();    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with DebugElement)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroDe); // click helper with DebugElement    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with native element)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroEl); // click helper with native element    expect(selectedHero).toBe(expectedHero);  });});//////////////////describe('DashboardHeroComponent when inside a test host', () => {  let testHost: TestHostComponent;  let fixture: ComponentFixture<TestHostComponent>;  let heroEl: HTMLElement;  beforeEach(waitForAsync(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  }));  beforeEach(() => {    // create TestHostComponent instead of DashboardHeroComponent    fixture = TestBed.createComponent(TestHostComponent);    testHost = fixture.componentInstance;    heroEl = fixture.nativeElement.querySelector('.hero');    fixture.detectChanges(); // trigger initial data binding  });  it('should display hero name', () => {    const expectedPipedName = testHost.hero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked', () => {    click(heroEl);    // selected hero should be the same data bound hero    expect(testHost.selectedHero).toBe(testHost.hero);  });});////// Test Host Component //////import {Component} from '@angular/core';@Component({  imports: [DashboardHeroComponent],  template: ` <dashboard-hero [hero]="hero" (selected)="onSelected($event)"> </dashboard-hero>`,})class TestHostComponent {  hero: Hero = {id: 42, name: 'Test Name'};  selectedHero: Hero | undefined;  onSelected(hero: Hero) {    this.selectedHero = hero;  }}

Helper click()

Clicar em um botão, uma âncora ou um elemento HTML arbitrário é uma tarefa de teste comum.

Torne isso consistente e direto encapsulando o processo de acionamento de clique em um helper como a seguinte função click():

testing/index.ts (click helper)

import {DebugElement} from '@angular/core';import {ComponentFixture, tick} from '@angular/core/testing';export * from './async-observable-helpers';export * from './jasmine-matchers';///// Short utilities //////** Wait a tick, then detect changes */export function advance(f: ComponentFixture<any>): void {  tick();  f.detectChanges();}// See https://developer.mozilla.org/docs/Web/API/MouseEvent/button/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */export const ButtonClickEvents = {  left: {button: 0},  right: {button: 2},};/** Simulate element click. Defaults to mouse left-button click event. */export function click(  el: DebugElement | HTMLElement,  eventObj: any = ButtonClickEvents.left,): void {  if (el instanceof HTMLElement) {    el.click();  } else {    el.triggerEventHandler('click', eventObj);  }}

O primeiro parâmetro é o elemento-para-clicar. Se quiser, passe um objeto de evento personalizado como segundo parâmetro. O padrão é um objeto de evento de mouse de botão esquerdo parcial aceito por muitos handlers incluindo a directive RouterLink.

IMPORTANTE: A função helper click() não é um dos utilitários de teste do Angular. É uma função definida no código de exemplo deste guia. Todos os testes de exemplo a usam. Se você gostar, adicione-a à sua própria coleção de helpers.

Aqui está o teste anterior, reescrito usando o helper click.

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

import {DebugElement} from '@angular/core';import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {first} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {appProviders} from '../app.config';import {Hero} from '../model/hero';import {DashboardHeroComponent} from './dashboard-hero.component';beforeEach(addMatchers);describe('DashboardHeroComponent when tested directly', () => {  let comp: DashboardHeroComponent;  let expectedHero: Hero;  let fixture: ComponentFixture<DashboardHeroComponent>;  let heroDe: DebugElement;  let heroEl: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  });  beforeEach(async () => {    fixture = TestBed.createComponent(DashboardHeroComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // find the hero's DebugElement and element    heroDe = fixture.debugElement.query(By.css('.hero'));    heroEl = heroDe.nativeElement;    // mock the hero supplied by the parent component    expectedHero = {id: 42, name: 'Test Name'};    // simulate the parent setting the input property with that hero    fixture.componentRef.setInput('hero', expectedHero);    // wait for initial data binding    await fixture.whenStable();  });  it('should display hero name in uppercase', () => {    const expectedPipedName = expectedHero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked (triggerEventHandler)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroDe.triggerEventHandler('click');    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (element.click)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroEl.click();    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with DebugElement)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroDe); // click helper with DebugElement    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with native element)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroEl); // click helper with native element    expect(selectedHero).toBe(expectedHero);  });});//////////////////describe('DashboardHeroComponent when inside a test host', () => {  let testHost: TestHostComponent;  let fixture: ComponentFixture<TestHostComponent>;  let heroEl: HTMLElement;  beforeEach(waitForAsync(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  }));  beforeEach(() => {    // create TestHostComponent instead of DashboardHeroComponent    fixture = TestBed.createComponent(TestHostComponent);    testHost = fixture.componentInstance;    heroEl = fixture.nativeElement.querySelector('.hero');    fixture.detectChanges(); // trigger initial data binding  });  it('should display hero name', () => {    const expectedPipedName = testHost.hero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked', () => {    click(heroEl);    // selected hero should be the same data bound hero    expect(testHost.selectedHero).toBe(testHost.hero);  });});////// Test Host Component //////import {Component} from '@angular/core';@Component({  imports: [DashboardHeroComponent],  template: ` <dashboard-hero [hero]="hero" (selected)="onSelected($event)"> </dashboard-hero>`,})class TestHostComponent {  hero: Hero = {id: 42, name: 'Test Name'};  selectedHero: Hero | undefined;  onSelected(hero: Hero) {    this.selectedHero = hero;  }}

Component dentro de um test host

Os testes anteriores desempenharam o papel do DashboardComponent host eles mesmos. Mas o DashboardHeroComponent funciona corretamente quando adequadamente vinculado por data-binding a um component host?

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

import {DebugElement} from '@angular/core';import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {first} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {appProviders} from '../app.config';import {Hero} from '../model/hero';import {DashboardHeroComponent} from './dashboard-hero.component';beforeEach(addMatchers);describe('DashboardHeroComponent when tested directly', () => {  let comp: DashboardHeroComponent;  let expectedHero: Hero;  let fixture: ComponentFixture<DashboardHeroComponent>;  let heroDe: DebugElement;  let heroEl: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  });  beforeEach(async () => {    fixture = TestBed.createComponent(DashboardHeroComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // find the hero's DebugElement and element    heroDe = fixture.debugElement.query(By.css('.hero'));    heroEl = heroDe.nativeElement;    // mock the hero supplied by the parent component    expectedHero = {id: 42, name: 'Test Name'};    // simulate the parent setting the input property with that hero    fixture.componentRef.setInput('hero', expectedHero);    // wait for initial data binding    await fixture.whenStable();  });  it('should display hero name in uppercase', () => {    const expectedPipedName = expectedHero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked (triggerEventHandler)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroDe.triggerEventHandler('click');    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (element.click)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroEl.click();    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with DebugElement)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroDe); // click helper with DebugElement    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with native element)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroEl); // click helper with native element    expect(selectedHero).toBe(expectedHero);  });});//////////////////describe('DashboardHeroComponent when inside a test host', () => {  let testHost: TestHostComponent;  let fixture: ComponentFixture<TestHostComponent>;  let heroEl: HTMLElement;  beforeEach(waitForAsync(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  }));  beforeEach(() => {    // create TestHostComponent instead of DashboardHeroComponent    fixture = TestBed.createComponent(TestHostComponent);    testHost = fixture.componentInstance;    heroEl = fixture.nativeElement.querySelector('.hero');    fixture.detectChanges(); // trigger initial data binding  });  it('should display hero name', () => {    const expectedPipedName = testHost.hero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked', () => {    click(heroEl);    // selected hero should be the same data bound hero    expect(testHost.selectedHero).toBe(testHost.hero);  });});////// Test Host Component //////import {Component} from '@angular/core';@Component({  imports: [DashboardHeroComponent],  template: ` <dashboard-hero [hero]="hero" (selected)="onSelected($event)"> </dashboard-hero>`,})class TestHostComponent {  hero: Hero = {id: 42, name: 'Test Name'};  selectedHero: Hero | undefined;  onSelected(hero: Hero) {    this.selectedHero = hero;  }}

O test host define a propriedade input hero do component com seu hero de teste. Ele vincula o evento selected do component com seu handler onSelected, que registra o hero emitido em sua propriedade selectedHero.

Mais tarde, os testes poderão verificar selectedHero para verificar que o evento DashboardHeroComponent.selected emitiu o hero esperado.

A configuração para os testes test-host é similar à configuração para os testes autônomos:

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

import {DebugElement} from '@angular/core';import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {first} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {appProviders} from '../app.config';import {Hero} from '../model/hero';import {DashboardHeroComponent} from './dashboard-hero.component';beforeEach(addMatchers);describe('DashboardHeroComponent when tested directly', () => {  let comp: DashboardHeroComponent;  let expectedHero: Hero;  let fixture: ComponentFixture<DashboardHeroComponent>;  let heroDe: DebugElement;  let heroEl: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  });  beforeEach(async () => {    fixture = TestBed.createComponent(DashboardHeroComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // find the hero's DebugElement and element    heroDe = fixture.debugElement.query(By.css('.hero'));    heroEl = heroDe.nativeElement;    // mock the hero supplied by the parent component    expectedHero = {id: 42, name: 'Test Name'};    // simulate the parent setting the input property with that hero    fixture.componentRef.setInput('hero', expectedHero);    // wait for initial data binding    await fixture.whenStable();  });  it('should display hero name in uppercase', () => {    const expectedPipedName = expectedHero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked (triggerEventHandler)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroDe.triggerEventHandler('click');    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (element.click)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroEl.click();    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with DebugElement)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroDe); // click helper with DebugElement    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with native element)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroEl); // click helper with native element    expect(selectedHero).toBe(expectedHero);  });});//////////////////describe('DashboardHeroComponent when inside a test host', () => {  let testHost: TestHostComponent;  let fixture: ComponentFixture<TestHostComponent>;  let heroEl: HTMLElement;  beforeEach(waitForAsync(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  }));  beforeEach(() => {    // create TestHostComponent instead of DashboardHeroComponent    fixture = TestBed.createComponent(TestHostComponent);    testHost = fixture.componentInstance;    heroEl = fixture.nativeElement.querySelector('.hero');    fixture.detectChanges(); // trigger initial data binding  });  it('should display hero name', () => {    const expectedPipedName = testHost.hero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked', () => {    click(heroEl);    // selected hero should be the same data bound hero    expect(testHost.selectedHero).toBe(testHost.hero);  });});////// Test Host Component //////import {Component} from '@angular/core';@Component({  imports: [DashboardHeroComponent],  template: ` <dashboard-hero [hero]="hero" (selected)="onSelected($event)"> </dashboard-hero>`,})class TestHostComponent {  hero: Hero = {id: 42, name: 'Test Name'};  selectedHero: Hero | undefined;  onSelected(hero: Hero) {    this.selectedHero = hero;  }}

Esta configuração do module de testes mostra duas diferenças importantes:

  • Ele cria o TestHostComponent em vez do DashboardHeroComponent
  • O TestHostComponent define o DashboardHeroComponent.hero com um binding

O createComponent retorna um fixture que contém uma instância de TestHostComponent em vez de uma instância de DashboardHeroComponent.

Criar o TestHostComponent tem o efeito colateral de criar um DashboardHeroComponent porque o último aparece dentro do template do primeiro. A query para o elemento hero (heroEl) ainda o encontra no DOM de teste, embora a maior profundidade na árvore de elementos do que antes.

Os próprios testes são quase idênticos à versão autônoma:

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

import {DebugElement} from '@angular/core';import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {first} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {appProviders} from '../app.config';import {Hero} from '../model/hero';import {DashboardHeroComponent} from './dashboard-hero.component';beforeEach(addMatchers);describe('DashboardHeroComponent when tested directly', () => {  let comp: DashboardHeroComponent;  let expectedHero: Hero;  let fixture: ComponentFixture<DashboardHeroComponent>;  let heroDe: DebugElement;  let heroEl: HTMLElement;  beforeEach(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  });  beforeEach(async () => {    fixture = TestBed.createComponent(DashboardHeroComponent);    fixture.autoDetectChanges();    comp = fixture.componentInstance;    // find the hero's DebugElement and element    heroDe = fixture.debugElement.query(By.css('.hero'));    heroEl = heroDe.nativeElement;    // mock the hero supplied by the parent component    expectedHero = {id: 42, name: 'Test Name'};    // simulate the parent setting the input property with that hero    fixture.componentRef.setInput('hero', expectedHero);    // wait for initial data binding    await fixture.whenStable();  });  it('should display hero name in uppercase', () => {    const expectedPipedName = expectedHero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked (triggerEventHandler)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroDe.triggerEventHandler('click');    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (element.click)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    heroEl.click();    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with DebugElement)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroDe); // click helper with DebugElement    expect(selectedHero).toBe(expectedHero);  });  it('should raise selected event when clicked (click helper with native element)', () => {    let selectedHero: Hero | undefined;    comp.selected.subscribe((hero: Hero) => (selectedHero = hero));    click(heroEl); // click helper with native element    expect(selectedHero).toBe(expectedHero);  });});//////////////////describe('DashboardHeroComponent when inside a test host', () => {  let testHost: TestHostComponent;  let fixture: ComponentFixture<TestHostComponent>;  let heroEl: HTMLElement;  beforeEach(waitForAsync(() => {    TestBed.configureTestingModule({      providers: appProviders,    });  }));  beforeEach(() => {    // create TestHostComponent instead of DashboardHeroComponent    fixture = TestBed.createComponent(TestHostComponent);    testHost = fixture.componentInstance;    heroEl = fixture.nativeElement.querySelector('.hero');    fixture.detectChanges(); // trigger initial data binding  });  it('should display hero name', () => {    const expectedPipedName = testHost.hero.name.toUpperCase();    expect(heroEl.textContent).toContain(expectedPipedName);  });  it('should raise selected event when clicked', () => {    click(heroEl);    // selected hero should be the same data bound hero    expect(testHost.selectedHero).toBe(testHost.hero);  });});////// Test Host Component //////import {Component} from '@angular/core';@Component({  imports: [DashboardHeroComponent],  template: ` <dashboard-hero [hero]="hero" (selected)="onSelected($event)"> </dashboard-hero>`,})class TestHostComponent {  hero: Hero = {id: 42, name: 'Test Name'};  selectedHero: Hero | undefined;  onSelected(hero: Hero) {    this.selectedHero = hero;  }}

Apenas o teste do evento selected difere. Ele confirma que o hero DashboardHeroComponent selecionado realmente encontra seu caminho através do event binding até o component host.

Routing component

Um routing component é um component que diz ao Router para navegar para outro component. O DashboardComponent é um routing component porque o usuário pode navegar para o HeroDetailComponent clicando em um dos botões de hero no dashboard.

O Angular fornece helpers de teste para reduzir boilerplate e testar código de forma mais eficaz que depende de HttpClient. A função provideRouter também pode ser usada diretamente no module de teste.

app/dashboard/dashboard.component.spec.ts

import {provideHttpClient} from '@angular/common/http';import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';import {NO_ERRORS_SCHEMA} from '@angular/core';import {TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {NavigationEnd, provideRouter, Router} from '@angular/router';import {RouterTestingHarness} from '@angular/router/testing';import {firstValueFrom} from 'rxjs';import {filter} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {HeroService} from '../model/hero.service';import {getTestHeroes} from '../model/testing/test-heroes';import {DashboardComponent} from './dashboard.component';import {appConfig} from '../app.config';import {HeroDetailComponent} from '../hero/hero-detail.component';beforeEach(addMatchers);let comp: DashboardComponent;let harness: RouterTestingHarness;////////  Deep  ////////////////describe('DashboardComponent (deep)', () => {  compileAndCreate();  tests(clickForDeep);  function clickForDeep() {    // get first <div class="hero">    const heroEl: HTMLElement = harness.routeNativeElement!.querySelector('.hero')!;    click(heroEl);    return firstValueFrom(      TestBed.inject(Router).events.pipe(filter((e) => e instanceof NavigationEnd)),    );  }});////////  Shallow ////////////////describe('DashboardComponent (shallow)', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [DashboardComponent, HeroDetailComponent],        providers: [provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}])],        schemas: [NO_ERRORS_SCHEMA],      }),    );  });  compileAndCreate();  tests(clickForShallow);  function clickForShallow() {    // get first <dashboard-hero> DebugElement    const heroDe = harness.routeDebugElement!.query(By.css('dashboard-hero'));    heroDe.triggerEventHandler('selected', comp.heroes[0]);    return Promise.resolve();  }});/** Add TestBed providers, compile, and create DashboardComponent */function compileAndCreate() {  beforeEach(async () => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [          provideRouter([{path: '**', component: DashboardComponent}]),          provideHttpClient(),          provideHttpClientTesting(),          HeroService,        ],      }),    );    harness = await RouterTestingHarness.create();    comp = await harness.navigateByUrl('/', DashboardComponent);    TestBed.inject(HttpTestingController).expectOne('api/heroes').flush(getTestHeroes());  });}/** * The (almost) same tests for both. * Only change: the way that the first hero is clicked */function tests(heroClick: () => Promise<unknown>) {  describe('after get dashboard heroes', () => {    let router: Router;    // Trigger component so it gets heroes and binds to them    beforeEach(waitForAsync(() => {      router = TestBed.inject(Router);      harness.detectChanges(); // runs ngOnInit -> getHeroes    }));    it('should HAVE heroes', () => {      expect(comp.heroes.length)        .withContext('should have heroes after service promise resolves')        .toBeGreaterThan(0);    });    it('should DISPLAY heroes', () => {      // Find and examine the displayed heroes      // Look for them in the DOM by css class      const heroes = harness.routeNativeElement!.querySelectorAll('dashboard-hero');      expect(heroes.length).withContext('should display 4 heroes').toBe(4);    });    it('should tell navigate when hero clicked', async () => {      await heroClick(); // trigger click on first inner <div class="hero">      // expecting to navigate to id of the component's first hero      const id = comp.heroes[0].id;      expect(TestBed.inject(Router).url)        .withContext('should nav to HeroDetail for first hero')        .toEqual(`/heroes/${id}`);    });  });}

O teste seguinte clica no hero exibido e confirma que navegamos para a URL esperada.

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

import {provideHttpClient} from '@angular/common/http';import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';import {NO_ERRORS_SCHEMA} from '@angular/core';import {TestBed, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {NavigationEnd, provideRouter, Router} from '@angular/router';import {RouterTestingHarness} from '@angular/router/testing';import {firstValueFrom} from 'rxjs';import {filter} from 'rxjs/operators';import {addMatchers, click} from '../../testing';import {HeroService} from '../model/hero.service';import {getTestHeroes} from '../model/testing/test-heroes';import {DashboardComponent} from './dashboard.component';import {appConfig} from '../app.config';import {HeroDetailComponent} from '../hero/hero-detail.component';beforeEach(addMatchers);let comp: DashboardComponent;let harness: RouterTestingHarness;////////  Deep  ////////////////describe('DashboardComponent (deep)', () => {  compileAndCreate();  tests(clickForDeep);  function clickForDeep() {    // get first <div class="hero">    const heroEl: HTMLElement = harness.routeNativeElement!.querySelector('.hero')!;    click(heroEl);    return firstValueFrom(      TestBed.inject(Router).events.pipe(filter((e) => e instanceof NavigationEnd)),    );  }});////////  Shallow ////////////////describe('DashboardComponent (shallow)', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [DashboardComponent, HeroDetailComponent],        providers: [provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}])],        schemas: [NO_ERRORS_SCHEMA],      }),    );  });  compileAndCreate();  tests(clickForShallow);  function clickForShallow() {    // get first <dashboard-hero> DebugElement    const heroDe = harness.routeDebugElement!.query(By.css('dashboard-hero'));    heroDe.triggerEventHandler('selected', comp.heroes[0]);    return Promise.resolve();  }});/** Add TestBed providers, compile, and create DashboardComponent */function compileAndCreate() {  beforeEach(async () => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [          provideRouter([{path: '**', component: DashboardComponent}]),          provideHttpClient(),          provideHttpClientTesting(),          HeroService,        ],      }),    );    harness = await RouterTestingHarness.create();    comp = await harness.navigateByUrl('/', DashboardComponent);    TestBed.inject(HttpTestingController).expectOne('api/heroes').flush(getTestHeroes());  });}/** * The (almost) same tests for both. * Only change: the way that the first hero is clicked */function tests(heroClick: () => Promise<unknown>) {  describe('after get dashboard heroes', () => {    let router: Router;    // Trigger component so it gets heroes and binds to them    beforeEach(waitForAsync(() => {      router = TestBed.inject(Router);      harness.detectChanges(); // runs ngOnInit -> getHeroes    }));    it('should HAVE heroes', () => {      expect(comp.heroes.length)        .withContext('should have heroes after service promise resolves')        .toBeGreaterThan(0);    });    it('should DISPLAY heroes', () => {      // Find and examine the displayed heroes      // Look for them in the DOM by css class      const heroes = harness.routeNativeElement!.querySelectorAll('dashboard-hero');      expect(heroes.length).withContext('should display 4 heroes').toBe(4);    });    it('should tell navigate when hero clicked', async () => {      await heroClick(); // trigger click on first inner <div class="hero">      // expecting to navigate to id of the component's first hero      const id = comp.heroes[0].id;      expect(TestBed.inject(Router).url)        .withContext('should nav to HeroDetail for first hero')        .toEqual(`/heroes/${id}`);    });  });}

Routed components

Um routed component é o destino de uma navegação do Router. Pode ser mais complicado de testar, especialmente quando a rota para o component inclui parâmetros. O HeroDetailComponent é um routed component que é o destino de tal rota.

Quando um usuário clica em um hero do Dashboard, o DashboardComponent diz ao Router para navegar para heroes/:id. O :id é um parâmetro de rota cujo valor é o id do hero a ser editado.

O Router corresponde aquela URL a uma rota para o HeroDetailComponent. Ele cria um objeto ActivatedRoute com as informações de roteamento e o injeta em uma nova instância do HeroDetailComponent.

Aqui estão os services injetados no HeroDetailComponent:

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

import {Component, inject} from '@angular/core';import {ActivatedRoute, Router, RouterLink} from '@angular/router';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailService} from './hero-detail.service';@Component({  selector: 'app-hero-detail',  templateUrl: './hero-detail.component.html',  styleUrls: ['./hero-detail.component.css'],  providers: [HeroDetailService],  imports: [...sharedImports],})export class HeroDetailComponent {  private heroDetailService = inject(HeroDetailService);  private route = inject(ActivatedRoute);  private router = inject(Router);  hero!: Hero;  constructor() {    // get hero when `id` param changes    this.route.paramMap.subscribe((pmap) => this.getHero(pmap.get('id')));  }  private getHero(id: string | null): void {    // when no id or id===0, create new blank hero    if (!id) {      this.hero = {id: 0, name: ''} as Hero;      return;    }    this.heroDetailService.getHero(id).subscribe((hero) => {      if (hero) {        this.hero = hero;      } else {        this.gotoList(); // id not found; navigate to list      }    });  }  save(): void {    this.heroDetailService.saveHero(this.hero).subscribe(() => this.gotoList());  }  cancel() {    this.gotoList();  }  gotoList() {    this.router.navigate(['../'], {relativeTo: this.route});  }}

O HeroDetail component precisa do parâmetro id para que possa buscar o hero correspondente usando o HeroDetailService. O component tem que obter o id da propriedade ActivatedRoute.paramMap que é um Observable.

Ele não pode simplesmente referenciar a propriedade id do ActivatedRoute.paramMap. O component tem que se inscrever no observable ActivatedRoute.paramMap e estar preparado para o id mudar durante seu tempo de vida.

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

import {Component, inject} from '@angular/core';import {ActivatedRoute, Router, RouterLink} from '@angular/router';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailService} from './hero-detail.service';@Component({  selector: 'app-hero-detail',  templateUrl: './hero-detail.component.html',  styleUrls: ['./hero-detail.component.css'],  providers: [HeroDetailService],  imports: [...sharedImports],})export class HeroDetailComponent {  private heroDetailService = inject(HeroDetailService);  private route = inject(ActivatedRoute);  private router = inject(Router);  hero!: Hero;  constructor() {    // get hero when `id` param changes    this.route.paramMap.subscribe((pmap) => this.getHero(pmap.get('id')));  }  private getHero(id: string | null): void {    // when no id or id===0, create new blank hero    if (!id) {      this.hero = {id: 0, name: ''} as Hero;      return;    }    this.heroDetailService.getHero(id).subscribe((hero) => {      if (hero) {        this.hero = hero;      } else {        this.gotoList(); // id not found; navigate to list      }    });  }  save(): void {    this.heroDetailService.saveHero(this.hero).subscribe(() => this.gotoList());  }  cancel() {    this.gotoList();  }  gotoList() {    this.router.navigate(['../'], {relativeTo: this.route});  }}

Os testes podem explorar como o HeroDetailComponent responde a diferentes valores do parâmetro id navegando para rotas diferentes.

Testes de components aninhados

Templates de components frequentemente têm components aninhados, cujos templates podem conter mais components.

A árvore de components pode ser muito profunda e às vezes os components aninhados não desempenham papel algum em testar o component no topo da árvore.

O AppComponent, por exemplo, exibe uma barra de navegação com âncoras e suas directives RouterLink.

app/app.component.html

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

Para validar os links mas não a navegação, você não precisa do Router para navegar e não precisa do <router-outlet> para marcar onde o Router insere routed components.

O BannerComponent e WelcomeComponent (indicados por <app-banner> e <app-welcome>) também são irrelevantes.

No entanto, qualquer teste que cria o AppComponent no DOM também cria instâncias desses três components e, se você deixar isso acontecer, terá que configurar o TestBed para criá-los.

Se você negligenciar declará-los, o compilador Angular não reconhecerá as tags <app-banner>, <app-welcome> e <router-outlet> no template AppComponent e lançará um erro.

Se você declarar os components reais, também terá que declarar seus components aninhados e fornecer todos os services injetados em qualquer component na árvore.

Esta seção descreve duas técnicas para minimizar a configuração. Use-as, sozinhas ou em combinação, para permanecer focado em testar o component primário.

Stubbing de components desnecessários

Na primeira técnica, você cria e declara versões stub dos components e directive que desempenham pouco ou nenhum papel nos testes.

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

import {Component, DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {provideRouter, Router, RouterLink, RouterOutlet} from '@angular/router';import {AppComponent} from './app.component';import {appConfig} from './app.config';import {UserService} from './model';import {WelcomeComponent} from './welcome/welcome.component';@Component({selector: 'app-banner', template: ''})class BannerStubComponent {}@Component({selector: 'router-outlet', template: ''})class RouterOutletStubComponent {}@Component({selector: 'app-welcome', template: ''})class WelcomeStubComponent {}let comp: AppComponent;let fixture: ComponentFixture<AppComponent>;describe('AppComponent & TestModule', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [BannerStubComponent, RouterLink, RouterOutletStubComponent, WelcomeStubComponent],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});//////// Testing w/ NO_ERRORS_SCHEMA //////describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [], // resets all imports        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      remove: {        imports: [RouterOutlet, WelcomeComponent],      },      set: {        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});function tests() {  let routerLinks: RouterLink[];  let linkDes: DebugElement[];  beforeEach(() => {    fixture.detectChanges(); // trigger initial data binding    // find DebugElements with an attached RouterLinkStubDirective    linkDes = fixture.debugElement.queryAll(By.directive(RouterLink));    // get attached link directive instances    // using each DebugElement's injector    routerLinks = linkDes.map((de) => de.injector.get(RouterLink));  });  it('can instantiate the component', () => {    expect(comp).not.toBeNull();  });  it('can get RouterLinks from template', () => {    expect(routerLinks.length).withContext('should have 3 routerLinks').toBe(3);    expect(routerLinks[0].href).toBe('/dashboard');    expect(routerLinks[1].href).toBe('/heroes');    expect(routerLinks[2].href).toBe('/about');  });  it('can click Heroes link in template', fakeAsync(() => {    const heroesLinkDe = linkDes[1]; // heroes link DebugElement    TestBed.inject(Router).resetConfig([{path: '**', children: []}]);    heroesLinkDe.triggerEventHandler('click', {button: 0});    tick();    fixture.detectChanges();    expect(TestBed.inject(Router).url).toBe('/heroes');  }));}

Os seletores stub correspondem aos seletores dos components reais correspondentes. Mas seus templates e classes estão vazios.

Então declare-os sobrescrevendo os imports do seu component usando TestBed.overrideComponent.

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

import {Component, DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {provideRouter, Router, RouterLink, RouterOutlet} from '@angular/router';import {AppComponent} from './app.component';import {appConfig} from './app.config';import {UserService} from './model';import {WelcomeComponent} from './welcome/welcome.component';@Component({selector: 'app-banner', template: ''})class BannerStubComponent {}@Component({selector: 'router-outlet', template: ''})class RouterOutletStubComponent {}@Component({selector: 'app-welcome', template: ''})class WelcomeStubComponent {}let comp: AppComponent;let fixture: ComponentFixture<AppComponent>;describe('AppComponent & TestModule', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [BannerStubComponent, RouterLink, RouterOutletStubComponent, WelcomeStubComponent],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});//////// Testing w/ NO_ERRORS_SCHEMA //////describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [], // resets all imports        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      remove: {        imports: [RouterOutlet, WelcomeComponent],      },      set: {        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});function tests() {  let routerLinks: RouterLink[];  let linkDes: DebugElement[];  beforeEach(() => {    fixture.detectChanges(); // trigger initial data binding    // find DebugElements with an attached RouterLinkStubDirective    linkDes = fixture.debugElement.queryAll(By.directive(RouterLink));    // get attached link directive instances    // using each DebugElement's injector    routerLinks = linkDes.map((de) => de.injector.get(RouterLink));  });  it('can instantiate the component', () => {    expect(comp).not.toBeNull();  });  it('can get RouterLinks from template', () => {    expect(routerLinks.length).withContext('should have 3 routerLinks').toBe(3);    expect(routerLinks[0].href).toBe('/dashboard');    expect(routerLinks[1].href).toBe('/heroes');    expect(routerLinks[2].href).toBe('/about');  });  it('can click Heroes link in template', fakeAsync(() => {    const heroesLinkDe = linkDes[1]; // heroes link DebugElement    TestBed.inject(Router).resetConfig([{path: '**', children: []}]);    heroesLinkDe.triggerEventHandler('click', {button: 0});    tick();    fixture.detectChanges();    expect(TestBed.inject(Router).url).toBe('/heroes');  }));}

ÚTIL: A chave set neste exemplo substitui todos os imports existentes no seu component, certifique-se de importar todas as dependências, não apenas os stubs. Alternativamente, você pode usar as chaves remove/add para remover e adicionar imports seletivamente.

NO_ERRORS_SCHEMA

Na segunda abordagem, adicione NO_ERRORS_SCHEMA aos overrides de metadata do seu component.

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

import {Component, DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {provideRouter, Router, RouterLink, RouterOutlet} from '@angular/router';import {AppComponent} from './app.component';import {appConfig} from './app.config';import {UserService} from './model';import {WelcomeComponent} from './welcome/welcome.component';@Component({selector: 'app-banner', template: ''})class BannerStubComponent {}@Component({selector: 'router-outlet', template: ''})class RouterOutletStubComponent {}@Component({selector: 'app-welcome', template: ''})class WelcomeStubComponent {}let comp: AppComponent;let fixture: ComponentFixture<AppComponent>;describe('AppComponent & TestModule', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [BannerStubComponent, RouterLink, RouterOutletStubComponent, WelcomeStubComponent],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});//////// Testing w/ NO_ERRORS_SCHEMA //////describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [], // resets all imports        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      remove: {        imports: [RouterOutlet, WelcomeComponent],      },      set: {        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});function tests() {  let routerLinks: RouterLink[];  let linkDes: DebugElement[];  beforeEach(() => {    fixture.detectChanges(); // trigger initial data binding    // find DebugElements with an attached RouterLinkStubDirective    linkDes = fixture.debugElement.queryAll(By.directive(RouterLink));    // get attached link directive instances    // using each DebugElement's injector    routerLinks = linkDes.map((de) => de.injector.get(RouterLink));  });  it('can instantiate the component', () => {    expect(comp).not.toBeNull();  });  it('can get RouterLinks from template', () => {    expect(routerLinks.length).withContext('should have 3 routerLinks').toBe(3);    expect(routerLinks[0].href).toBe('/dashboard');    expect(routerLinks[1].href).toBe('/heroes');    expect(routerLinks[2].href).toBe('/about');  });  it('can click Heroes link in template', fakeAsync(() => {    const heroesLinkDe = linkDes[1]; // heroes link DebugElement    TestBed.inject(Router).resetConfig([{path: '**', children: []}]);    heroesLinkDe.triggerEventHandler('click', {button: 0});    tick();    fixture.detectChanges();    expect(TestBed.inject(Router).url).toBe('/heroes');  }));}

O NO_ERRORS_SCHEMA diz ao compilador Angular para ignorar elementos e atributos não reconhecidos.

O compilador reconhece o elemento <app-root> e o atributo routerLink porque você declarou um AppComponent e RouterLink correspondentes na configuração do TestBed.

Mas o compilador não lançará um erro quando encontrar <app-banner>, <app-welcome> ou <router-outlet>. Ele simplesmente os renderiza como tags vazias e o browser os ignora.

Você não precisa mais dos stub components.

Use ambas as técnicas juntas

Estas são técnicas para Shallow Component Testing, assim chamadas porque reduzem a superfície visual do component apenas àqueles elementos no template do component que importam para os testes.

A abordagem NO_ERRORS_SCHEMA é a mais fácil das duas, mas não a use em excesso.

O NO_ERRORS_SCHEMA também impede que o compilador diga sobre os components e atributos faltantes que você omitiu inadvertidamente ou digitou incorretamente. Você pode desperdiçar horas perseguindo bugs fantasmas que o compilador teria capturado em um instante.

A abordagem de stub component tem outra vantagem. Embora os stubs neste exemplo estivessem vazios, você poderia dar a eles templates e classes simplificados se seus testes precisarem interagir com eles de alguma forma.

Na prática, você combinará as duas técnicas na mesma configuração, como visto neste exemplo.

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

import {Component, DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {provideRouter, Router, RouterLink, RouterOutlet} from '@angular/router';import {AppComponent} from './app.component';import {appConfig} from './app.config';import {UserService} from './model';import {WelcomeComponent} from './welcome/welcome.component';@Component({selector: 'app-banner', template: ''})class BannerStubComponent {}@Component({selector: 'router-outlet', template: ''})class RouterOutletStubComponent {}@Component({selector: 'app-welcome', template: ''})class WelcomeStubComponent {}let comp: AppComponent;let fixture: ComponentFixture<AppComponent>;describe('AppComponent & TestModule', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [BannerStubComponent, RouterLink, RouterOutletStubComponent, WelcomeStubComponent],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});//////// Testing w/ NO_ERRORS_SCHEMA //////describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [], // resets all imports        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      remove: {        imports: [RouterOutlet, WelcomeComponent],      },      set: {        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});function tests() {  let routerLinks: RouterLink[];  let linkDes: DebugElement[];  beforeEach(() => {    fixture.detectChanges(); // trigger initial data binding    // find DebugElements with an attached RouterLinkStubDirective    linkDes = fixture.debugElement.queryAll(By.directive(RouterLink));    // get attached link directive instances    // using each DebugElement's injector    routerLinks = linkDes.map((de) => de.injector.get(RouterLink));  });  it('can instantiate the component', () => {    expect(comp).not.toBeNull();  });  it('can get RouterLinks from template', () => {    expect(routerLinks.length).withContext('should have 3 routerLinks').toBe(3);    expect(routerLinks[0].href).toBe('/dashboard');    expect(routerLinks[1].href).toBe('/heroes');    expect(routerLinks[2].href).toBe('/about');  });  it('can click Heroes link in template', fakeAsync(() => {    const heroesLinkDe = linkDes[1]; // heroes link DebugElement    TestBed.inject(Router).resetConfig([{path: '**', children: []}]);    heroesLinkDe.triggerEventHandler('click', {button: 0});    tick();    fixture.detectChanges();    expect(TestBed.inject(Router).url).toBe('/heroes');  }));}

O compilador Angular cria o BannerStubComponent para o elemento <app-banner> e aplica o RouterLink às âncoras com o atributo routerLink, mas ignora as tags <app-welcome> e <router-outlet>.

By.directive e directives injetadas

Um pouco mais de configuração aciona o data binding inicial e obtém referências aos links de navegação:

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

import {Component, DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {provideRouter, Router, RouterLink, RouterOutlet} from '@angular/router';import {AppComponent} from './app.component';import {appConfig} from './app.config';import {UserService} from './model';import {WelcomeComponent} from './welcome/welcome.component';@Component({selector: 'app-banner', template: ''})class BannerStubComponent {}@Component({selector: 'router-outlet', template: ''})class RouterOutletStubComponent {}@Component({selector: 'app-welcome', template: ''})class WelcomeStubComponent {}let comp: AppComponent;let fixture: ComponentFixture<AppComponent>;describe('AppComponent & TestModule', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [BannerStubComponent, RouterLink, RouterOutletStubComponent, WelcomeStubComponent],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});//////// Testing w/ NO_ERRORS_SCHEMA //////describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [], // resets all imports        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      remove: {        imports: [RouterOutlet, WelcomeComponent],      },      set: {        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});function tests() {  let routerLinks: RouterLink[];  let linkDes: DebugElement[];  beforeEach(() => {    fixture.detectChanges(); // trigger initial data binding    // find DebugElements with an attached RouterLinkStubDirective    linkDes = fixture.debugElement.queryAll(By.directive(RouterLink));    // get attached link directive instances    // using each DebugElement's injector    routerLinks = linkDes.map((de) => de.injector.get(RouterLink));  });  it('can instantiate the component', () => {    expect(comp).not.toBeNull();  });  it('can get RouterLinks from template', () => {    expect(routerLinks.length).withContext('should have 3 routerLinks').toBe(3);    expect(routerLinks[0].href).toBe('/dashboard');    expect(routerLinks[1].href).toBe('/heroes');    expect(routerLinks[2].href).toBe('/about');  });  it('can click Heroes link in template', fakeAsync(() => {    const heroesLinkDe = linkDes[1]; // heroes link DebugElement    TestBed.inject(Router).resetConfig([{path: '**', children: []}]);    heroesLinkDe.triggerEventHandler('click', {button: 0});    tick();    fixture.detectChanges();    expect(TestBed.inject(Router).url).toBe('/heroes');  }));}

Três pontos de interesse especial:

  • Localize os elementos âncora com uma directive anexada usando By.directive
  • A query retorna wrappers DebugElement em torno dos elementos correspondentes
  • Cada DebugElement expõe um dependency injector com a instância específica da directive anexada àquele elemento

Os links do AppComponent a validar são os seguintes:

app/app.component.html (navigation links)

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

Aqui estão alguns testes que confirmam que esses links estão conectados às directives routerLink conforme esperado:

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

import {Component, DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {provideRouter, Router, RouterLink, RouterOutlet} from '@angular/router';import {AppComponent} from './app.component';import {appConfig} from './app.config';import {UserService} from './model';import {WelcomeComponent} from './welcome/welcome.component';@Component({selector: 'app-banner', template: ''})class BannerStubComponent {}@Component({selector: 'router-outlet', template: ''})class RouterOutletStubComponent {}@Component({selector: 'app-welcome', template: ''})class WelcomeStubComponent {}let comp: AppComponent;let fixture: ComponentFixture<AppComponent>;describe('AppComponent & TestModule', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [BannerStubComponent, RouterLink, RouterOutletStubComponent, WelcomeStubComponent],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});//////// Testing w/ NO_ERRORS_SCHEMA //////describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      set: {        imports: [], // resets all imports        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});describe('AppComponent & NO_ERRORS_SCHEMA', () => {  beforeEach(() => {    TestBed.configureTestingModule(      Object.assign({}, appConfig, {        providers: [provideRouter([]), UserService],      }),    ).overrideComponent(AppComponent, {      remove: {        imports: [RouterOutlet, WelcomeComponent],      },      set: {        schemas: [NO_ERRORS_SCHEMA],      },    });    fixture = TestBed.createComponent(AppComponent);    comp = fixture.componentInstance;  });  tests();});function tests() {  let routerLinks: RouterLink[];  let linkDes: DebugElement[];  beforeEach(() => {    fixture.detectChanges(); // trigger initial data binding    // find DebugElements with an attached RouterLinkStubDirective    linkDes = fixture.debugElement.queryAll(By.directive(RouterLink));    // get attached link directive instances    // using each DebugElement's injector    routerLinks = linkDes.map((de) => de.injector.get(RouterLink));  });  it('can instantiate the component', () => {    expect(comp).not.toBeNull();  });  it('can get RouterLinks from template', () => {    expect(routerLinks.length).withContext('should have 3 routerLinks').toBe(3);    expect(routerLinks[0].href).toBe('/dashboard');    expect(routerLinks[1].href).toBe('/heroes');    expect(routerLinks[2].href).toBe('/about');  });  it('can click Heroes link in template', fakeAsync(() => {    const heroesLinkDe = linkDes[1]; // heroes link DebugElement    TestBed.inject(Router).resetConfig([{path: '**', children: []}]);    heroesLinkDe.triggerEventHandler('click', {button: 0});    tick();    fixture.detectChanges();    expect(TestBed.inject(Router).url).toBe('/heroes');  }));}

Usar um objeto page

O HeroDetailComponent é uma view simples com um título, dois campos hero e dois botões.

Mas há bastante complexidade de template mesmo neste formulário simples.

app/hero/hero-detail.component.html

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

Testes que exercitam o component precisam …

  • Esperar até que um hero chegue antes que elementos apareçam no DOM
  • Uma referência ao texto do título
  • Uma referência à caixa de input do nome para inspecioná-la e defini-la
  • Referências aos dois botões para que possam clicar neles

Mesmo um formulário pequeno como este pode produzir uma confusão de configuração condicional torturada e seleção de elementos CSS.

Domar a complexidade com uma classe Page que lida com acesso a propriedades do component e encapsula a lógica que as define.

Aqui está tal classe Page para o hero-detail.component.spec.ts

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

import {HttpClient, HttpHandler, provideHttpClient} from '@angular/common/http';import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';import {fakeAsync, TestBed, tick} from '@angular/core/testing';import {provideRouter, Router} from '@angular/router';import {RouterTestingHarness} from '@angular/router/testing';import {asyncData, click} from '../../testing';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailComponent} from './hero-detail.component';import {HeroDetailService} from './hero-detail.service';import {HeroListComponent} from './hero-list.component';////// Testing Vars //////let component: HeroDetailComponent;let harness: RouterTestingHarness;let page: Page;////// Tests //////describe('HeroDetailComponent', () => {  describe('with HeroModule setup', heroModuleSetup);  describe('when override its provided HeroDetailService', overrideSetup);  describe('with FormsModule setup', formsModuleSetup);  describe('with SharedModule setup', sharedModuleSetup);});///////////////////const testHero = getTestHeroes()[0];function overrideSetup() {  class HeroDetailServiceSpy {    testHero: Hero = {...testHero};    /* emit cloned test hero */    getHero = jasmine      .createSpy('getHero')      .and.callFake(() => asyncData(Object.assign({}, this.testHero)));    /* emit clone of test hero, with changes merged in */    saveHero = jasmine      .createSpy('saveHero')      .and.callFake((hero: Hero) => asyncData(Object.assign(this.testHero, hero)));  }  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes', component: HeroListComponent},            {path: 'heroes/:id', component: HeroDetailComponent},          ]),          HttpClient,          HttpHandler,          // HeroDetailService at this level is IRRELEVANT!          {provide: HeroDetailService, useValue: {}},        ],      }),    )      .overrideComponent(HeroDetailComponent, {        set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]},      });  });  let hdsSpy: HeroDetailServiceSpy;  beforeEach(async () => {    harness = await RouterTestingHarness.create();    component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetailComponent);    page = new Page();    // get the component's injected HeroDetailServiceSpy    hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any;    harness.detectChanges();  });  it('should have called `getHero`', () => {    expect(hdsSpy.getHero.calls.count())      .withContext('getHero called once')      .toBe(1, 'getHero called once');  });  it("should display stub hero's name", () => {    expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);  });  it('should save stub hero change', fakeAsync(() => {    const origName = hdsSpy.testHero.name;    const newName = 'New Name';    page.nameInput.value = newName;    page.nameInput.dispatchEvent(new Event('input')); // tell Angular    expect(component.hero.name).withContext('component hero has new name').toBe(newName);    expect(hdsSpy.testHero.name).withContext('service hero unchanged before save').toBe(origName);    click(page.saveBtn);    expect(hdsSpy.saveHero.calls.count()).withContext('saveHero called once').toBe(1);    tick(); // wait for async save to complete    expect(hdsSpy.testHero.name).withContext('service hero has new name after save').toBe(newName);    expect(TestBed.inject(Router).url).toEqual('/heroes');  }));}////////////////////import {getTestHeroes} from '../model/testing/test-hero.service';const firstHero = getTestHeroes()[0];function heroModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes/:id', component: HeroDetailComponent},            {path: 'heroes', component: HeroListComponent},          ]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  describe('when navigate to existing hero', () => {    let expectedHero: Hero;    beforeEach(async () => {      expectedHero = firstHero;      await createComponent(expectedHero.id);    });    it("should display that hero's name", () => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });    it('should navigate when click cancel', () => {      click(page.cancelBtn);      expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`);    });    it('should save when click save but not navigate immediately', () => {      click(page.saveBtn);      expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'}));      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    });    it('should navigate when click save and save resolves', fakeAsync(() => {      click(page.saveBtn);      tick(); // wait for async save to complete      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    }));    it('should convert hero name to Title Case', async () => {      harness.fixture.autoDetectChanges();      // get the name's input and display elements from the DOM      const hostElement: HTMLElement = harness.routeNativeElement!;      const nameInput: HTMLInputElement = hostElement.querySelector('input')!;      const nameDisplay: HTMLElement = hostElement.querySelector('span')!;      // simulate user entering a new name into the input box      nameInput.value = 'quick BROWN  fOx';      // Dispatch a DOM event so that Angular learns of input value change.      nameInput.dispatchEvent(new Event('input'));      // Wait for Angular to update the display binding through the title pipe      await harness.fixture.whenStable();      expect(nameDisplay.textContent).toBe('Quick Brown  Fox');    });  });  describe('when navigate to non-existent hero id', () => {    beforeEach(async () => {      await createComponent(999);    });    it('should try to navigate back to hero list', () => {      expect(TestBed.inject(Router).url).toEqual('/heroes');    });  });}/////////////////////import {FormsModule} from '@angular/forms';import {TitleCasePipe} from '../shared/title-case.pipe';import {appConfig} from '../app.config';function formsModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [FormsModule, HeroDetailComponent, TitleCasePipe],        providers: [          provideHttpClient(),          provideHttpClientTesting(),          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}///////////////////////function sharedModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, sharedImports],        providers: [          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}/////////// Helpers //////** Create the HeroDetailComponent, initialize it, set test variables  */async function createComponent(id: number) {  harness = await RouterTestingHarness.create();  component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetailComponent);  page = new Page();  const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`);  const hero = getTestHeroes().find((h) => h.id === Number(id));  request.flush(hero ? [hero] : []);  harness.detectChanges();}class Page {  // getter properties wait to query the DOM until called.  get buttons() {    return this.queryAll<HTMLButtonElement>('button');  }  get saveBtn() {    return this.buttons[0];  }  get cancelBtn() {    return this.buttons[1];  }  get nameDisplay() {    return this.query<HTMLElement>('span');  }  get nameInput() {    return this.query<HTMLInputElement>('input');  }  //// query helpers ////  private query<T>(selector: string): T {    return harness.routeNativeElement!.querySelector(selector)! as T;  }  private queryAll<T>(selector: string): T[] {    return harness.routeNativeElement!.querySelectorAll(selector) as any as T[];  }}

Agora os hooks importantes para manipulação e inspeção do component estão organizados de forma organizada e acessíveis a partir de uma instância de Page.

Um método createComponent cria um objeto page e preenche os espaços em branco uma vez que o hero chega.

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

import {HttpClient, HttpHandler, provideHttpClient} from '@angular/common/http';import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';import {fakeAsync, TestBed, tick} from '@angular/core/testing';import {provideRouter, Router} from '@angular/router';import {RouterTestingHarness} from '@angular/router/testing';import {asyncData, click} from '../../testing';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailComponent} from './hero-detail.component';import {HeroDetailService} from './hero-detail.service';import {HeroListComponent} from './hero-list.component';////// Testing Vars //////let component: HeroDetailComponent;let harness: RouterTestingHarness;let page: Page;////// Tests //////describe('HeroDetailComponent', () => {  describe('with HeroModule setup', heroModuleSetup);  describe('when override its provided HeroDetailService', overrideSetup);  describe('with FormsModule setup', formsModuleSetup);  describe('with SharedModule setup', sharedModuleSetup);});///////////////////const testHero = getTestHeroes()[0];function overrideSetup() {  class HeroDetailServiceSpy {    testHero: Hero = {...testHero};    /* emit cloned test hero */    getHero = jasmine      .createSpy('getHero')      .and.callFake(() => asyncData(Object.assign({}, this.testHero)));    /* emit clone of test hero, with changes merged in */    saveHero = jasmine      .createSpy('saveHero')      .and.callFake((hero: Hero) => asyncData(Object.assign(this.testHero, hero)));  }  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes', component: HeroListComponent},            {path: 'heroes/:id', component: HeroDetailComponent},          ]),          HttpClient,          HttpHandler,          // HeroDetailService at this level is IRRELEVANT!          {provide: HeroDetailService, useValue: {}},        ],      }),    )      .overrideComponent(HeroDetailComponent, {        set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]},      });  });  let hdsSpy: HeroDetailServiceSpy;  beforeEach(async () => {    harness = await RouterTestingHarness.create();    component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetailComponent);    page = new Page();    // get the component's injected HeroDetailServiceSpy    hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any;    harness.detectChanges();  });  it('should have called `getHero`', () => {    expect(hdsSpy.getHero.calls.count())      .withContext('getHero called once')      .toBe(1, 'getHero called once');  });  it("should display stub hero's name", () => {    expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);  });  it('should save stub hero change', fakeAsync(() => {    const origName = hdsSpy.testHero.name;    const newName = 'New Name';    page.nameInput.value = newName;    page.nameInput.dispatchEvent(new Event('input')); // tell Angular    expect(component.hero.name).withContext('component hero has new name').toBe(newName);    expect(hdsSpy.testHero.name).withContext('service hero unchanged before save').toBe(origName);    click(page.saveBtn);    expect(hdsSpy.saveHero.calls.count()).withContext('saveHero called once').toBe(1);    tick(); // wait for async save to complete    expect(hdsSpy.testHero.name).withContext('service hero has new name after save').toBe(newName);    expect(TestBed.inject(Router).url).toEqual('/heroes');  }));}////////////////////import {getTestHeroes} from '../model/testing/test-hero.service';const firstHero = getTestHeroes()[0];function heroModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes/:id', component: HeroDetailComponent},            {path: 'heroes', component: HeroListComponent},          ]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  describe('when navigate to existing hero', () => {    let expectedHero: Hero;    beforeEach(async () => {      expectedHero = firstHero;      await createComponent(expectedHero.id);    });    it("should display that hero's name", () => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });    it('should navigate when click cancel', () => {      click(page.cancelBtn);      expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`);    });    it('should save when click save but not navigate immediately', () => {      click(page.saveBtn);      expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'}));      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    });    it('should navigate when click save and save resolves', fakeAsync(() => {      click(page.saveBtn);      tick(); // wait for async save to complete      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    }));    it('should convert hero name to Title Case', async () => {      harness.fixture.autoDetectChanges();      // get the name's input and display elements from the DOM      const hostElement: HTMLElement = harness.routeNativeElement!;      const nameInput: HTMLInputElement = hostElement.querySelector('input')!;      const nameDisplay: HTMLElement = hostElement.querySelector('span')!;      // simulate user entering a new name into the input box      nameInput.value = 'quick BROWN  fOx';      // Dispatch a DOM event so that Angular learns of input value change.      nameInput.dispatchEvent(new Event('input'));      // Wait for Angular to update the display binding through the title pipe      await harness.fixture.whenStable();      expect(nameDisplay.textContent).toBe('Quick Brown  Fox');    });  });  describe('when navigate to non-existent hero id', () => {    beforeEach(async () => {      await createComponent(999);    });    it('should try to navigate back to hero list', () => {      expect(TestBed.inject(Router).url).toEqual('/heroes');    });  });}/////////////////////import {FormsModule} from '@angular/forms';import {TitleCasePipe} from '../shared/title-case.pipe';import {appConfig} from '../app.config';function formsModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [FormsModule, HeroDetailComponent, TitleCasePipe],        providers: [          provideHttpClient(),          provideHttpClientTesting(),          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}///////////////////////function sharedModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, sharedImports],        providers: [          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}/////////// Helpers //////** Create the HeroDetailComponent, initialize it, set test variables  */async function createComponent(id: number) {  harness = await RouterTestingHarness.create();  component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetailComponent);  page = new Page();  const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`);  const hero = getTestHeroes().find((h) => h.id === Number(id));  request.flush(hero ? [hero] : []);  harness.detectChanges();}class Page {  // getter properties wait to query the DOM until called.  get buttons() {    return this.queryAll<HTMLButtonElement>('button');  }  get saveBtn() {    return this.buttons[0];  }  get cancelBtn() {    return this.buttons[1];  }  get nameDisplay() {    return this.query<HTMLElement>('span');  }  get nameInput() {    return this.query<HTMLInputElement>('input');  }  //// query helpers ////  private query<T>(selector: string): T {    return harness.routeNativeElement!.querySelector(selector)! as T;  }  private queryAll<T>(selector: string): T[] {    return harness.routeNativeElement!.querySelectorAll(selector) as any as T[];  }}

Aqui estão mais alguns testes HeroDetailComponent para reforçar o ponto.

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

import {HttpClient, HttpHandler, provideHttpClient} from '@angular/common/http';import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';import {fakeAsync, TestBed, tick} from '@angular/core/testing';import {provideRouter, Router} from '@angular/router';import {RouterTestingHarness} from '@angular/router/testing';import {asyncData, click} from '../../testing';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailComponent} from './hero-detail.component';import {HeroDetailService} from './hero-detail.service';import {HeroListComponent} from './hero-list.component';////// Testing Vars //////let component: HeroDetailComponent;let harness: RouterTestingHarness;let page: Page;////// Tests //////describe('HeroDetailComponent', () => {  describe('with HeroModule setup', heroModuleSetup);  describe('when override its provided HeroDetailService', overrideSetup);  describe('with FormsModule setup', formsModuleSetup);  describe('with SharedModule setup', sharedModuleSetup);});///////////////////const testHero = getTestHeroes()[0];function overrideSetup() {  class HeroDetailServiceSpy {    testHero: Hero = {...testHero};    /* emit cloned test hero */    getHero = jasmine      .createSpy('getHero')      .and.callFake(() => asyncData(Object.assign({}, this.testHero)));    /* emit clone of test hero, with changes merged in */    saveHero = jasmine      .createSpy('saveHero')      .and.callFake((hero: Hero) => asyncData(Object.assign(this.testHero, hero)));  }  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes', component: HeroListComponent},            {path: 'heroes/:id', component: HeroDetailComponent},          ]),          HttpClient,          HttpHandler,          // HeroDetailService at this level is IRRELEVANT!          {provide: HeroDetailService, useValue: {}},        ],      }),    )      .overrideComponent(HeroDetailComponent, {        set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]},      });  });  let hdsSpy: HeroDetailServiceSpy;  beforeEach(async () => {    harness = await RouterTestingHarness.create();    component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetailComponent);    page = new Page();    // get the component's injected HeroDetailServiceSpy    hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any;    harness.detectChanges();  });  it('should have called `getHero`', () => {    expect(hdsSpy.getHero.calls.count())      .withContext('getHero called once')      .toBe(1, 'getHero called once');  });  it("should display stub hero's name", () => {    expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);  });  it('should save stub hero change', fakeAsync(() => {    const origName = hdsSpy.testHero.name;    const newName = 'New Name';    page.nameInput.value = newName;    page.nameInput.dispatchEvent(new Event('input')); // tell Angular    expect(component.hero.name).withContext('component hero has new name').toBe(newName);    expect(hdsSpy.testHero.name).withContext('service hero unchanged before save').toBe(origName);    click(page.saveBtn);    expect(hdsSpy.saveHero.calls.count()).withContext('saveHero called once').toBe(1);    tick(); // wait for async save to complete    expect(hdsSpy.testHero.name).withContext('service hero has new name after save').toBe(newName);    expect(TestBed.inject(Router).url).toEqual('/heroes');  }));}////////////////////import {getTestHeroes} from '../model/testing/test-hero.service';const firstHero = getTestHeroes()[0];function heroModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes/:id', component: HeroDetailComponent},            {path: 'heroes', component: HeroListComponent},          ]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  describe('when navigate to existing hero', () => {    let expectedHero: Hero;    beforeEach(async () => {      expectedHero = firstHero;      await createComponent(expectedHero.id);    });    it("should display that hero's name", () => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });    it('should navigate when click cancel', () => {      click(page.cancelBtn);      expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`);    });    it('should save when click save but not navigate immediately', () => {      click(page.saveBtn);      expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'}));      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    });    it('should navigate when click save and save resolves', fakeAsync(() => {      click(page.saveBtn);      tick(); // wait for async save to complete      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    }));    it('should convert hero name to Title Case', async () => {      harness.fixture.autoDetectChanges();      // get the name's input and display elements from the DOM      const hostElement: HTMLElement = harness.routeNativeElement!;      const nameInput: HTMLInputElement = hostElement.querySelector('input')!;      const nameDisplay: HTMLElement = hostElement.querySelector('span')!;      // simulate user entering a new name into the input box      nameInput.value = 'quick BROWN  fOx';      // Dispatch a DOM event so that Angular learns of input value change.      nameInput.dispatchEvent(new Event('input'));      // Wait for Angular to update the display binding through the title pipe      await harness.fixture.whenStable();      expect(nameDisplay.textContent).toBe('Quick Brown  Fox');    });  });  describe('when navigate to non-existent hero id', () => {    beforeEach(async () => {      await createComponent(999);    });    it('should try to navigate back to hero list', () => {      expect(TestBed.inject(Router).url).toEqual('/heroes');    });  });}/////////////////////import {FormsModule} from '@angular/forms';import {TitleCasePipe} from '../shared/title-case.pipe';import {appConfig} from '../app.config';function formsModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [FormsModule, HeroDetailComponent, TitleCasePipe],        providers: [          provideHttpClient(),          provideHttpClientTesting(),          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}///////////////////////function sharedModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, sharedImports],        providers: [          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}/////////// Helpers //////** Create the HeroDetailComponent, initialize it, set test variables  */async function createComponent(id: number) {  harness = await RouterTestingHarness.create();  component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetailComponent);  page = new Page();  const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`);  const hero = getTestHeroes().find((h) => h.id === Number(id));  request.flush(hero ? [hero] : []);  harness.detectChanges();}class Page {  // getter properties wait to query the DOM until called.  get buttons() {    return this.queryAll<HTMLButtonElement>('button');  }  get saveBtn() {    return this.buttons[0];  }  get cancelBtn() {    return this.buttons[1];  }  get nameDisplay() {    return this.query<HTMLElement>('span');  }  get nameInput() {    return this.query<HTMLInputElement>('input');  }  //// query helpers ////  private query<T>(selector: string): T {    return harness.routeNativeElement!.querySelector(selector)! as T;  }  private queryAll<T>(selector: string): T[] {    return harness.routeNativeElement!.querySelectorAll(selector) as any as T[];  }}

Override de component providers

O HeroDetailComponent fornece seu próprio HeroDetailService.

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

import {Component, inject} from '@angular/core';import {ActivatedRoute, Router, RouterLink} from '@angular/router';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailService} from './hero-detail.service';@Component({  selector: 'app-hero-detail',  templateUrl: './hero-detail.component.html',  styleUrls: ['./hero-detail.component.css'],  providers: [HeroDetailService],  imports: [...sharedImports],})export class HeroDetailComponent {  private heroDetailService = inject(HeroDetailService);  private route = inject(ActivatedRoute);  private router = inject(Router);  hero!: Hero;  constructor() {    // get hero when `id` param changes    this.route.paramMap.subscribe((pmap) => this.getHero(pmap.get('id')));  }  private getHero(id: string | null): void {    // when no id or id===0, create new blank hero    if (!id) {      this.hero = {id: 0, name: ''} as Hero;      return;    }    this.heroDetailService.getHero(id).subscribe((hero) => {      if (hero) {        this.hero = hero;      } else {        this.gotoList(); // id not found; navigate to list      }    });  }  save(): void {    this.heroDetailService.saveHero(this.hero).subscribe(() => this.gotoList());  }  cancel() {    this.gotoList();  }  gotoList() {    this.router.navigate(['../'], {relativeTo: this.route});  }}

Não é possível fazer stub do HeroDetailService do component nos providers do TestBed.configureTestingModule. Esses são providers para o module de teste, não o component. Eles preparam o dependency injector no nível do fixture.

O Angular cria o component com seu próprio injector, que é um filho do injector do fixture. Ele registra os providers do component (o HeroDetailService neste caso) com o injector filho.

Um teste não pode obter services do injector filho a partir do injector do fixture. E TestBed.configureTestingModule também não pode configurá-los.

O Angular criou novas instâncias do HeroDetailService real o tempo todo!

ÚTIL: Esses testes podem falhar ou expirar se o HeroDetailService fizer suas próprias chamadas XHR a um servidor remoto. Pode não haver um servidor remoto para chamar.

Felizmente, o HeroDetailService delega responsabilidade por acesso a dados remotos a um HeroService injetado.

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

import {inject, Injectable} from '@angular/core';import {Observable} from 'rxjs';import {map} from 'rxjs/operators';import {Hero} from '../model/hero';import {HeroService} from '../model/hero.service';@Injectable({providedIn: 'root'})export class HeroDetailService {  private heroService = inject(HeroService);  // Returns a clone which caller may modify safely  getHero(id: number | string): Observable<Hero | null> {    if (typeof id === 'string') {      id = parseInt(id, 10);    }    return this.heroService.getHero(id).pipe(      map((hero) => (hero ? Object.assign({}, hero) : null)), // clone or null    );  }  saveHero(hero: Hero) {    return this.heroService.updateHero(hero);  }}

A configuração de teste anterior substitui o HeroService real por um TestHeroService que intercepta requisições do servidor e falsifica suas respostas.

E se você não tiver tanta sorte. E se falsificar o HeroService for difícil? E se HeroDetailService fizer suas próprias requisições ao servidor?

O método TestBed.overrideComponent pode substituir os providers do component por test doubles fáceis de gerenciar como visto na seguinte variação de configuração:

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

import {HttpClient, HttpHandler, provideHttpClient} from '@angular/common/http';import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';import {fakeAsync, TestBed, tick} from '@angular/core/testing';import {provideRouter, Router} from '@angular/router';import {RouterTestingHarness} from '@angular/router/testing';import {asyncData, click} from '../../testing';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailComponent} from './hero-detail.component';import {HeroDetailService} from './hero-detail.service';import {HeroListComponent} from './hero-list.component';////// Testing Vars //////let component: HeroDetailComponent;let harness: RouterTestingHarness;let page: Page;////// Tests //////describe('HeroDetailComponent', () => {  describe('with HeroModule setup', heroModuleSetup);  describe('when override its provided HeroDetailService', overrideSetup);  describe('with FormsModule setup', formsModuleSetup);  describe('with SharedModule setup', sharedModuleSetup);});///////////////////const testHero = getTestHeroes()[0];function overrideSetup() {  class HeroDetailServiceSpy {    testHero: Hero = {...testHero};    /* emit cloned test hero */    getHero = jasmine      .createSpy('getHero')      .and.callFake(() => asyncData(Object.assign({}, this.testHero)));    /* emit clone of test hero, with changes merged in */    saveHero = jasmine      .createSpy('saveHero')      .and.callFake((hero: Hero) => asyncData(Object.assign(this.testHero, hero)));  }  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes', component: HeroListComponent},            {path: 'heroes/:id', component: HeroDetailComponent},          ]),          HttpClient,          HttpHandler,          // HeroDetailService at this level is IRRELEVANT!          {provide: HeroDetailService, useValue: {}},        ],      }),    )      .overrideComponent(HeroDetailComponent, {        set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]},      });  });  let hdsSpy: HeroDetailServiceSpy;  beforeEach(async () => {    harness = await RouterTestingHarness.create();    component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetailComponent);    page = new Page();    // get the component's injected HeroDetailServiceSpy    hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any;    harness.detectChanges();  });  it('should have called `getHero`', () => {    expect(hdsSpy.getHero.calls.count())      .withContext('getHero called once')      .toBe(1, 'getHero called once');  });  it("should display stub hero's name", () => {    expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);  });  it('should save stub hero change', fakeAsync(() => {    const origName = hdsSpy.testHero.name;    const newName = 'New Name';    page.nameInput.value = newName;    page.nameInput.dispatchEvent(new Event('input')); // tell Angular    expect(component.hero.name).withContext('component hero has new name').toBe(newName);    expect(hdsSpy.testHero.name).withContext('service hero unchanged before save').toBe(origName);    click(page.saveBtn);    expect(hdsSpy.saveHero.calls.count()).withContext('saveHero called once').toBe(1);    tick(); // wait for async save to complete    expect(hdsSpy.testHero.name).withContext('service hero has new name after save').toBe(newName);    expect(TestBed.inject(Router).url).toEqual('/heroes');  }));}////////////////////import {getTestHeroes} from '../model/testing/test-hero.service';const firstHero = getTestHeroes()[0];function heroModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes/:id', component: HeroDetailComponent},            {path: 'heroes', component: HeroListComponent},          ]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  describe('when navigate to existing hero', () => {    let expectedHero: Hero;    beforeEach(async () => {      expectedHero = firstHero;      await createComponent(expectedHero.id);    });    it("should display that hero's name", () => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });    it('should navigate when click cancel', () => {      click(page.cancelBtn);      expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`);    });    it('should save when click save but not navigate immediately', () => {      click(page.saveBtn);      expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'}));      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    });    it('should navigate when click save and save resolves', fakeAsync(() => {      click(page.saveBtn);      tick(); // wait for async save to complete      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    }));    it('should convert hero name to Title Case', async () => {      harness.fixture.autoDetectChanges();      // get the name's input and display elements from the DOM      const hostElement: HTMLElement = harness.routeNativeElement!;      const nameInput: HTMLInputElement = hostElement.querySelector('input')!;      const nameDisplay: HTMLElement = hostElement.querySelector('span')!;      // simulate user entering a new name into the input box      nameInput.value = 'quick BROWN  fOx';      // Dispatch a DOM event so that Angular learns of input value change.      nameInput.dispatchEvent(new Event('input'));      // Wait for Angular to update the display binding through the title pipe      await harness.fixture.whenStable();      expect(nameDisplay.textContent).toBe('Quick Brown  Fox');    });  });  describe('when navigate to non-existent hero id', () => {    beforeEach(async () => {      await createComponent(999);    });    it('should try to navigate back to hero list', () => {      expect(TestBed.inject(Router).url).toEqual('/heroes');    });  });}/////////////////////import {FormsModule} from '@angular/forms';import {TitleCasePipe} from '../shared/title-case.pipe';import {appConfig} from '../app.config';function formsModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [FormsModule, HeroDetailComponent, TitleCasePipe],        providers: [          provideHttpClient(),          provideHttpClientTesting(),          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}///////////////////////function sharedModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, sharedImports],        providers: [          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}/////////// Helpers //////** Create the HeroDetailComponent, initialize it, set test variables  */async function createComponent(id: number) {  harness = await RouterTestingHarness.create();  component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetailComponent);  page = new Page();  const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`);  const hero = getTestHeroes().find((h) => h.id === Number(id));  request.flush(hero ? [hero] : []);  harness.detectChanges();}class Page {  // getter properties wait to query the DOM until called.  get buttons() {    return this.queryAll<HTMLButtonElement>('button');  }  get saveBtn() {    return this.buttons[0];  }  get cancelBtn() {    return this.buttons[1];  }  get nameDisplay() {    return this.query<HTMLElement>('span');  }  get nameInput() {    return this.query<HTMLInputElement>('input');  }  //// query helpers ////  private query<T>(selector: string): T {    return harness.routeNativeElement!.querySelector(selector)! as T;  }  private queryAll<T>(selector: string): T[] {    return harness.routeNativeElement!.querySelectorAll(selector) as any as T[];  }}

Observe que TestBed.configureTestingModule não fornece mais um HeroService falso porque não é necessário.

O método overrideComponent

Foque no método overrideComponent.

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

import {HttpClient, HttpHandler, provideHttpClient} from '@angular/common/http';import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';import {fakeAsync, TestBed, tick} from '@angular/core/testing';import {provideRouter, Router} from '@angular/router';import {RouterTestingHarness} from '@angular/router/testing';import {asyncData, click} from '../../testing';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailComponent} from './hero-detail.component';import {HeroDetailService} from './hero-detail.service';import {HeroListComponent} from './hero-list.component';////// Testing Vars //////let component: HeroDetailComponent;let harness: RouterTestingHarness;let page: Page;////// Tests //////describe('HeroDetailComponent', () => {  describe('with HeroModule setup', heroModuleSetup);  describe('when override its provided HeroDetailService', overrideSetup);  describe('with FormsModule setup', formsModuleSetup);  describe('with SharedModule setup', sharedModuleSetup);});///////////////////const testHero = getTestHeroes()[0];function overrideSetup() {  class HeroDetailServiceSpy {    testHero: Hero = {...testHero};    /* emit cloned test hero */    getHero = jasmine      .createSpy('getHero')      .and.callFake(() => asyncData(Object.assign({}, this.testHero)));    /* emit clone of test hero, with changes merged in */    saveHero = jasmine      .createSpy('saveHero')      .and.callFake((hero: Hero) => asyncData(Object.assign(this.testHero, hero)));  }  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes', component: HeroListComponent},            {path: 'heroes/:id', component: HeroDetailComponent},          ]),          HttpClient,          HttpHandler,          // HeroDetailService at this level is IRRELEVANT!          {provide: HeroDetailService, useValue: {}},        ],      }),    )      .overrideComponent(HeroDetailComponent, {        set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]},      });  });  let hdsSpy: HeroDetailServiceSpy;  beforeEach(async () => {    harness = await RouterTestingHarness.create();    component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetailComponent);    page = new Page();    // get the component's injected HeroDetailServiceSpy    hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any;    harness.detectChanges();  });  it('should have called `getHero`', () => {    expect(hdsSpy.getHero.calls.count())      .withContext('getHero called once')      .toBe(1, 'getHero called once');  });  it("should display stub hero's name", () => {    expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);  });  it('should save stub hero change', fakeAsync(() => {    const origName = hdsSpy.testHero.name;    const newName = 'New Name';    page.nameInput.value = newName;    page.nameInput.dispatchEvent(new Event('input')); // tell Angular    expect(component.hero.name).withContext('component hero has new name').toBe(newName);    expect(hdsSpy.testHero.name).withContext('service hero unchanged before save').toBe(origName);    click(page.saveBtn);    expect(hdsSpy.saveHero.calls.count()).withContext('saveHero called once').toBe(1);    tick(); // wait for async save to complete    expect(hdsSpy.testHero.name).withContext('service hero has new name after save').toBe(newName);    expect(TestBed.inject(Router).url).toEqual('/heroes');  }));}////////////////////import {getTestHeroes} from '../model/testing/test-hero.service';const firstHero = getTestHeroes()[0];function heroModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes/:id', component: HeroDetailComponent},            {path: 'heroes', component: HeroListComponent},          ]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  describe('when navigate to existing hero', () => {    let expectedHero: Hero;    beforeEach(async () => {      expectedHero = firstHero;      await createComponent(expectedHero.id);    });    it("should display that hero's name", () => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });    it('should navigate when click cancel', () => {      click(page.cancelBtn);      expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`);    });    it('should save when click save but not navigate immediately', () => {      click(page.saveBtn);      expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'}));      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    });    it('should navigate when click save and save resolves', fakeAsync(() => {      click(page.saveBtn);      tick(); // wait for async save to complete      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    }));    it('should convert hero name to Title Case', async () => {      harness.fixture.autoDetectChanges();      // get the name's input and display elements from the DOM      const hostElement: HTMLElement = harness.routeNativeElement!;      const nameInput: HTMLInputElement = hostElement.querySelector('input')!;      const nameDisplay: HTMLElement = hostElement.querySelector('span')!;      // simulate user entering a new name into the input box      nameInput.value = 'quick BROWN  fOx';      // Dispatch a DOM event so that Angular learns of input value change.      nameInput.dispatchEvent(new Event('input'));      // Wait for Angular to update the display binding through the title pipe      await harness.fixture.whenStable();      expect(nameDisplay.textContent).toBe('Quick Brown  Fox');    });  });  describe('when navigate to non-existent hero id', () => {    beforeEach(async () => {      await createComponent(999);    });    it('should try to navigate back to hero list', () => {      expect(TestBed.inject(Router).url).toEqual('/heroes');    });  });}/////////////////////import {FormsModule} from '@angular/forms';import {TitleCasePipe} from '../shared/title-case.pipe';import {appConfig} from '../app.config';function formsModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [FormsModule, HeroDetailComponent, TitleCasePipe],        providers: [          provideHttpClient(),          provideHttpClientTesting(),          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}///////////////////////function sharedModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, sharedImports],        providers: [          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}/////////// Helpers //////** Create the HeroDetailComponent, initialize it, set test variables  */async function createComponent(id: number) {  harness = await RouterTestingHarness.create();  component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetailComponent);  page = new Page();  const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`);  const hero = getTestHeroes().find((h) => h.id === Number(id));  request.flush(hero ? [hero] : []);  harness.detectChanges();}class Page {  // getter properties wait to query the DOM until called.  get buttons() {    return this.queryAll<HTMLButtonElement>('button');  }  get saveBtn() {    return this.buttons[0];  }  get cancelBtn() {    return this.buttons[1];  }  get nameDisplay() {    return this.query<HTMLElement>('span');  }  get nameInput() {    return this.query<HTMLInputElement>('input');  }  //// query helpers ////  private query<T>(selector: string): T {    return harness.routeNativeElement!.querySelector(selector)! as T;  }  private queryAll<T>(selector: string): T[] {    return harness.routeNativeElement!.querySelectorAll(selector) as any as T[];  }}

Ele recebe dois argumentos: o tipo de component a sobrescrever (HeroDetailComponent) e um objeto de metadata de override. O objeto de metadata de override é um genérico definido da seguinte forma:

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

Um objeto de metadata de override pode adicionar e remover elementos nas propriedades de metadata ou redefinir completamente essas propriedades. Este exemplo redefine a metadata providers do component.

O parâmetro de tipo, T, é o tipo de metadata que você passaria para o decorator @Component:

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

Fornecer um spy stub (HeroDetailServiceSpy)

Este exemplo substitui completamente o array providers do component por um novo array contendo um HeroDetailServiceSpy.

O HeroDetailServiceSpy é uma versão stubbed do HeroDetailService real que falsifica todos os recursos necessários daquele service. Ele nem injeta nem delega para o HeroService de nível inferior, então não há necessidade de fornecer um test double para isso.

Os testes HeroDetailComponent relacionados afirmarão que métodos do HeroDetailService foram chamados espiando os métodos do service. Consequentemente, o stub implementa seus métodos como spies:

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

import {HttpClient, HttpHandler, provideHttpClient} from '@angular/common/http';import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';import {fakeAsync, TestBed, tick} from '@angular/core/testing';import {provideRouter, Router} from '@angular/router';import {RouterTestingHarness} from '@angular/router/testing';import {asyncData, click} from '../../testing';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailComponent} from './hero-detail.component';import {HeroDetailService} from './hero-detail.service';import {HeroListComponent} from './hero-list.component';////// Testing Vars //////let component: HeroDetailComponent;let harness: RouterTestingHarness;let page: Page;////// Tests //////describe('HeroDetailComponent', () => {  describe('with HeroModule setup', heroModuleSetup);  describe('when override its provided HeroDetailService', overrideSetup);  describe('with FormsModule setup', formsModuleSetup);  describe('with SharedModule setup', sharedModuleSetup);});///////////////////const testHero = getTestHeroes()[0];function overrideSetup() {  class HeroDetailServiceSpy {    testHero: Hero = {...testHero};    /* emit cloned test hero */    getHero = jasmine      .createSpy('getHero')      .and.callFake(() => asyncData(Object.assign({}, this.testHero)));    /* emit clone of test hero, with changes merged in */    saveHero = jasmine      .createSpy('saveHero')      .and.callFake((hero: Hero) => asyncData(Object.assign(this.testHero, hero)));  }  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes', component: HeroListComponent},            {path: 'heroes/:id', component: HeroDetailComponent},          ]),          HttpClient,          HttpHandler,          // HeroDetailService at this level is IRRELEVANT!          {provide: HeroDetailService, useValue: {}},        ],      }),    )      .overrideComponent(HeroDetailComponent, {        set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]},      });  });  let hdsSpy: HeroDetailServiceSpy;  beforeEach(async () => {    harness = await RouterTestingHarness.create();    component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetailComponent);    page = new Page();    // get the component's injected HeroDetailServiceSpy    hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any;    harness.detectChanges();  });  it('should have called `getHero`', () => {    expect(hdsSpy.getHero.calls.count())      .withContext('getHero called once')      .toBe(1, 'getHero called once');  });  it("should display stub hero's name", () => {    expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);  });  it('should save stub hero change', fakeAsync(() => {    const origName = hdsSpy.testHero.name;    const newName = 'New Name';    page.nameInput.value = newName;    page.nameInput.dispatchEvent(new Event('input')); // tell Angular    expect(component.hero.name).withContext('component hero has new name').toBe(newName);    expect(hdsSpy.testHero.name).withContext('service hero unchanged before save').toBe(origName);    click(page.saveBtn);    expect(hdsSpy.saveHero.calls.count()).withContext('saveHero called once').toBe(1);    tick(); // wait for async save to complete    expect(hdsSpy.testHero.name).withContext('service hero has new name after save').toBe(newName);    expect(TestBed.inject(Router).url).toEqual('/heroes');  }));}////////////////////import {getTestHeroes} from '../model/testing/test-hero.service';const firstHero = getTestHeroes()[0];function heroModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes/:id', component: HeroDetailComponent},            {path: 'heroes', component: HeroListComponent},          ]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  describe('when navigate to existing hero', () => {    let expectedHero: Hero;    beforeEach(async () => {      expectedHero = firstHero;      await createComponent(expectedHero.id);    });    it("should display that hero's name", () => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });    it('should navigate when click cancel', () => {      click(page.cancelBtn);      expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`);    });    it('should save when click save but not navigate immediately', () => {      click(page.saveBtn);      expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'}));      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    });    it('should navigate when click save and save resolves', fakeAsync(() => {      click(page.saveBtn);      tick(); // wait for async save to complete      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    }));    it('should convert hero name to Title Case', async () => {      harness.fixture.autoDetectChanges();      // get the name's input and display elements from the DOM      const hostElement: HTMLElement = harness.routeNativeElement!;      const nameInput: HTMLInputElement = hostElement.querySelector('input')!;      const nameDisplay: HTMLElement = hostElement.querySelector('span')!;      // simulate user entering a new name into the input box      nameInput.value = 'quick BROWN  fOx';      // Dispatch a DOM event so that Angular learns of input value change.      nameInput.dispatchEvent(new Event('input'));      // Wait for Angular to update the display binding through the title pipe      await harness.fixture.whenStable();      expect(nameDisplay.textContent).toBe('Quick Brown  Fox');    });  });  describe('when navigate to non-existent hero id', () => {    beforeEach(async () => {      await createComponent(999);    });    it('should try to navigate back to hero list', () => {      expect(TestBed.inject(Router).url).toEqual('/heroes');    });  });}/////////////////////import {FormsModule} from '@angular/forms';import {TitleCasePipe} from '../shared/title-case.pipe';import {appConfig} from '../app.config';function formsModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [FormsModule, HeroDetailComponent, TitleCasePipe],        providers: [          provideHttpClient(),          provideHttpClientTesting(),          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}///////////////////////function sharedModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, sharedImports],        providers: [          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}/////////// Helpers //////** Create the HeroDetailComponent, initialize it, set test variables  */async function createComponent(id: number) {  harness = await RouterTestingHarness.create();  component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetailComponent);  page = new Page();  const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`);  const hero = getTestHeroes().find((h) => h.id === Number(id));  request.flush(hero ? [hero] : []);  harness.detectChanges();}class Page {  // getter properties wait to query the DOM until called.  get buttons() {    return this.queryAll<HTMLButtonElement>('button');  }  get saveBtn() {    return this.buttons[0];  }  get cancelBtn() {    return this.buttons[1];  }  get nameDisplay() {    return this.query<HTMLElement>('span');  }  get nameInput() {    return this.query<HTMLInputElement>('input');  }  //// query helpers ////  private query<T>(selector: string): T {    return harness.routeNativeElement!.querySelector(selector)! as T;  }  private queryAll<T>(selector: string): T[] {    return harness.routeNativeElement!.querySelectorAll(selector) as any as T[];  }}

Os testes de override

Agora os testes podem controlar o hero do component diretamente manipulando o testHero do spy-stub e confirmar que métodos do service foram chamados.

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

import {HttpClient, HttpHandler, provideHttpClient} from '@angular/common/http';import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing';import {fakeAsync, TestBed, tick} from '@angular/core/testing';import {provideRouter, Router} from '@angular/router';import {RouterTestingHarness} from '@angular/router/testing';import {asyncData, click} from '../../testing';import {Hero} from '../model/hero';import {sharedImports} from '../shared/shared';import {HeroDetailComponent} from './hero-detail.component';import {HeroDetailService} from './hero-detail.service';import {HeroListComponent} from './hero-list.component';////// Testing Vars //////let component: HeroDetailComponent;let harness: RouterTestingHarness;let page: Page;////// Tests //////describe('HeroDetailComponent', () => {  describe('with HeroModule setup', heroModuleSetup);  describe('when override its provided HeroDetailService', overrideSetup);  describe('with FormsModule setup', formsModuleSetup);  describe('with SharedModule setup', sharedModuleSetup);});///////////////////const testHero = getTestHeroes()[0];function overrideSetup() {  class HeroDetailServiceSpy {    testHero: Hero = {...testHero};    /* emit cloned test hero */    getHero = jasmine      .createSpy('getHero')      .and.callFake(() => asyncData(Object.assign({}, this.testHero)));    /* emit clone of test hero, with changes merged in */    saveHero = jasmine      .createSpy('saveHero')      .and.callFake((hero: Hero) => asyncData(Object.assign(this.testHero, hero)));  }  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes', component: HeroListComponent},            {path: 'heroes/:id', component: HeroDetailComponent},          ]),          HttpClient,          HttpHandler,          // HeroDetailService at this level is IRRELEVANT!          {provide: HeroDetailService, useValue: {}},        ],      }),    )      .overrideComponent(HeroDetailComponent, {        set: {providers: [{provide: HeroDetailService, useClass: HeroDetailServiceSpy}]},      });  });  let hdsSpy: HeroDetailServiceSpy;  beforeEach(async () => {    harness = await RouterTestingHarness.create();    component = await harness.navigateByUrl(`/heroes/${testHero.id}`, HeroDetailComponent);    page = new Page();    // get the component's injected HeroDetailServiceSpy    hdsSpy = harness.routeDebugElement!.injector.get(HeroDetailService) as any;    harness.detectChanges();  });  it('should have called `getHero`', () => {    expect(hdsSpy.getHero.calls.count())      .withContext('getHero called once')      .toBe(1, 'getHero called once');  });  it("should display stub hero's name", () => {    expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);  });  it('should save stub hero change', fakeAsync(() => {    const origName = hdsSpy.testHero.name;    const newName = 'New Name';    page.nameInput.value = newName;    page.nameInput.dispatchEvent(new Event('input')); // tell Angular    expect(component.hero.name).withContext('component hero has new name').toBe(newName);    expect(hdsSpy.testHero.name).withContext('service hero unchanged before save').toBe(origName);    click(page.saveBtn);    expect(hdsSpy.saveHero.calls.count()).withContext('saveHero called once').toBe(1);    tick(); // wait for async save to complete    expect(hdsSpy.testHero.name).withContext('service hero has new name after save').toBe(newName);    expect(TestBed.inject(Router).url).toEqual('/heroes');  }));}////////////////////import {getTestHeroes} from '../model/testing/test-hero.service';const firstHero = getTestHeroes()[0];function heroModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, HeroListComponent],        providers: [          provideRouter([            {path: 'heroes/:id', component: HeroDetailComponent},            {path: 'heroes', component: HeroListComponent},          ]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  describe('when navigate to existing hero', () => {    let expectedHero: Hero;    beforeEach(async () => {      expectedHero = firstHero;      await createComponent(expectedHero.id);    });    it("should display that hero's name", () => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });    it('should navigate when click cancel', () => {      click(page.cancelBtn);      expect(TestBed.inject(Router).url).toEqual(`/heroes/${expectedHero.id}`);    });    it('should save when click save but not navigate immediately', () => {      click(page.saveBtn);      expect(TestBed.inject(HttpTestingController).expectOne({method: 'PUT', url: 'api/heroes'}));      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    });    it('should navigate when click save and save resolves', fakeAsync(() => {      click(page.saveBtn);      tick(); // wait for async save to complete      expect(TestBed.inject(Router).url).toEqual('/heroes/41');    }));    it('should convert hero name to Title Case', async () => {      harness.fixture.autoDetectChanges();      // get the name's input and display elements from the DOM      const hostElement: HTMLElement = harness.routeNativeElement!;      const nameInput: HTMLInputElement = hostElement.querySelector('input')!;      const nameDisplay: HTMLElement = hostElement.querySelector('span')!;      // simulate user entering a new name into the input box      nameInput.value = 'quick BROWN  fOx';      // Dispatch a DOM event so that Angular learns of input value change.      nameInput.dispatchEvent(new Event('input'));      // Wait for Angular to update the display binding through the title pipe      await harness.fixture.whenStable();      expect(nameDisplay.textContent).toBe('Quick Brown  Fox');    });  });  describe('when navigate to non-existent hero id', () => {    beforeEach(async () => {      await createComponent(999);    });    it('should try to navigate back to hero list', () => {      expect(TestBed.inject(Router).url).toEqual('/heroes');    });  });}/////////////////////import {FormsModule} from '@angular/forms';import {TitleCasePipe} from '../shared/title-case.pipe';import {appConfig} from '../app.config';function formsModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [FormsModule, HeroDetailComponent, TitleCasePipe],        providers: [          provideHttpClient(),          provideHttpClientTesting(),          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}///////////////////////function sharedModuleSetup() {  beforeEach(async () => {    await TestBed.configureTestingModule(      Object.assign({}, appConfig, {        imports: [HeroDetailComponent, sharedImports],        providers: [          provideRouter([{path: 'heroes/:id', component: HeroDetailComponent}]),          provideHttpClient(),          provideHttpClientTesting(),        ],      }),    );  });  it("should display 1st hero's name", async () => {    const expectedHero = firstHero;    await createComponent(expectedHero.id).then(() => {      expect(page.nameDisplay.textContent).toBe(expectedHero.name);    });  });}/////////// Helpers //////** Create the HeroDetailComponent, initialize it, set test variables  */async function createComponent(id: number) {  harness = await RouterTestingHarness.create();  component = await harness.navigateByUrl(`/heroes/${id}`, HeroDetailComponent);  page = new Page();  const request = TestBed.inject(HttpTestingController).expectOne(`api/heroes/?id=${id}`);  const hero = getTestHeroes().find((h) => h.id === Number(id));  request.flush(hero ? [hero] : []);  harness.detectChanges();}class Page {  // getter properties wait to query the DOM until called.  get buttons() {    return this.queryAll<HTMLButtonElement>('button');  }  get saveBtn() {    return this.buttons[0];  }  get cancelBtn() {    return this.buttons[1];  }  get nameDisplay() {    return this.query<HTMLElement>('span');  }  get nameInput() {    return this.query<HTMLInputElement>('input');  }  //// query helpers ////  private query<T>(selector: string): T {    return harness.routeNativeElement!.querySelector(selector)! as T;  }  private queryAll<T>(selector: string): T[] {    return harness.routeNativeElement!.querySelectorAll(selector) as any as T[];  }}

Mais overrides

O método TestBed.overrideComponent pode ser chamado várias vezes para os mesmos ou diferentes components. O TestBed oferece métodos similares overrideDirective, overrideModule e overridePipe para cavar e substituir partes dessas outras classes.

Explore as opções e combinações por conta própria.