Guías Detalladas
Pruebas

Escenarios de prueba de componentes

Esta guía explora casos de uso comunes de prueba de componentes.

Binding de componentes

En la aplicación de ejemplo, el BannerComponent presenta texto de título estático en la plantilla HTML.

Después de algunos cambios, el BannerComponent presenta un título dinámico vinculándose a la propiedad title del componente como esto.

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 mínimo que sea esto, decides agregar una prueba para confirmar que el componente realmente muestra el contenido correcto donde crees que debería.

Consultar por el <h1>

Escribirás una secuencia de pruebas que inspeccionan el valor del elemento <h1> que envuelve el binding de interpolación de la propiedad title.

Actualizas el beforeEach para encontrar ese elemento con un querySelector HTML estándar y asignarlo a la variable 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() no vincula datos

Para tu primera prueba te gustaría ver que la pantalla muestra el title predeterminado. Tu instinto es escribir una prueba que inmediatamente inspecciona el <h1> así:

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');  });});

Esa prueba falla con el mensaje:

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

El binding ocurre cuando Angular realiza detección de cambios.

En producción, la detección de cambios se activa automáticamente cuando Angular crea un componente o el usuario ingresa una pulsación de tecla, por ejemplo.

El TestBed.createComponent no desencadena detección de cambios por defecto; un hecho confirmado en la prueba revisada:

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()

Puedes decirle al TestBed que realice binding de datos llamando a fixture.detectChanges(). Solo entonces el <h1> tiene el 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');  });});

La detección de cambios retrasada es intencional y útil. Le da al probador una oportunidad de inspeccionar y cambiar el estado del componente antes de que Angular inicie el binding de datos y llame a hooks de ciclo de vida.

Aquí hay otra prueba que cambia la propiedad title del componente antes de llamar a 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');  });});

Detección automática de cambios

Las pruebas de BannerComponent llaman frecuentemente a detectChanges. Muchos probadores prefieren que el entorno de prueba de Angular ejecute la detección de cambios automáticamente como lo hace en producción.

Eso es posible configurando el TestBed con el provider ComponentFixtureAutoDetect. Primero impórtalo de la librería utilitaria de testing:

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);  });});

Luego agrégalo al array providers de la configuración del módulo de testing:

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: También puedes usar la función fixture.autoDetectChanges() en su lugar si solo quieres habilitar la detección automática de cambios después de hacer actualizaciones al estado del componente del fixture. Además, la detección automática de cambios está activada por defecto cuando usas provideZonelessChangeDetection y desactivarla no es recomendado.

Aquí hay tres pruebas que ilustran cómo funciona la detección automática de cambios.

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);  });});

La primera prueba muestra el beneficio de la detección automática de cambios.

La segunda y tercera prueba revelan una limitación importante. El entorno de testing de Angular no ejecuta detección de cambios síncronamente cuando las actualizaciones ocurren dentro del caso de prueba que cambió el title del componente. La prueba debe llamar a await fixture.whenStable para esperar otra ronda de detección de cambios.

ÚTIL: Angular no sabe sobre actualizaciones directas a valores que no son signals. La forma más fácil de asegurar que la detección de cambios se programe es usar signals para valores leídos en la plantilla.

Cambiar un valor de input con dispatchEvent()

Para simular la entrada del usuario, encuentra el elemento input y establece su propiedad value.

Pero hay un paso esencial, intermedio.

Angular no sabe que estableciste la propiedad value del elemento input. No leerá esa propiedad hasta que generes el evento input del elemento llamando a dispatchEvent().

El siguiente ejemplo demuestra la secuencia adecuada.

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[];  }}

Componente con archivos externos

El BannerComponent precedente está definido con una plantilla inline y css inline, especificados en las propiedades @Component.template y @Component.styles respectivamente.

Muchos componentes especifican plantillas externas y css externo con las propiedades @Component.templateUrl y @Component.styleUrls respectivamente, como lo hace la siguiente variante de BannerComponent.

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 sintaxis le dice al compilador de Angular que lea los archivos externos durante la compilación del componente.

Eso no es un problema cuando ejecutas el comando CLI ng test porque compila la aplicación antes de ejecutar las pruebas.

Sin embargo, si ejecutas las pruebas en un entorno no-CLI, las pruebas de este componente podrían fallar. Por ejemplo, si ejecutas las pruebas de BannerComponent en un entorno de codificación web como plunker, verás un mensaje como este:

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.

Obtienes este mensaje de fallo de prueba cuando el entorno de runtime compila el código fuente durante las pruebas mismas.

Para corregir el problema, llama a compileComponents() como se explica en la siguiente sección Llamando compileComponents.

Componente con una dependencia

Los componentes a menudo tienen dependencias de servicio.

El WelcomeComponent muestra un mensaje de bienvenida al usuario autenticado. Sabe quién es el usuario basado en una propiedad del UserService inyectado:

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.',    );  }}

El WelcomeComponent tiene lógica de decisión que interactúa con el servicio, lógica que hace que este componente valga la pena probar.

Proporcionar dobles de prueba de servicio

Un componente-bajo-prueba no tiene que ser proporcionado con servicios reales.

Inyectar el UserService real podría ser difícil. El servicio real podría pedirle al usuario credenciales de inicio de sesión e intentar llegar a un servidor de autenticación. Estos comportamientos pueden ser difíciles de interceptar. Ten en cuenta que usar dobles de prueba hace que la prueba se comporte diferente de la producción, así que úsalos con moderación.

Obtener servicios inyectados

Las pruebas necesitan acceso al UserService inyectado en el WelcomeComponent.

Angular tiene un sistema de inyección jerárquico. Puede haber inyectores en múltiples niveles, desde el inyector raíz creado por el TestBed hacia abajo a través del árbol de componentes.

La forma más segura de obtener el servicio inyectado, la forma que siempre funciona, es obtenerlo del inyector del componente-bajo-prueba. El inyector del componente es una propiedad del DebugElement del 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: Esto usualmente no es necesario. Los servicios a menudo se proporcionan en la raíz o los overrides del TestBed y pueden recuperarse más fácilmente con TestBed.inject() (ver abajo).

TestBed.inject()

Esto es más fácil de recordar y menos verboso que recuperar un servicio usando el DebugElement del fixture.

En esta suite de pruebas, el único provider de UserService es el módulo de testing raíz, por lo que es seguro llamar a TestBed.inject() de la siguiente manera:

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 un caso de uso en el que TestBed.inject() no funciona, consulta la sección Override component providers que explica cuándo y por qué debes obtener el servicio del inyector del componente en su lugar.

Configuración final y pruebas

Aquí está el 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);  });});

Y aquí hay algunas pruebas:

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);  });});

La primera es una prueba de cordura; confirma que el UserService es llamado y funciona.

ÚTIL: La función withContext (por ejemplo, 'expected name') es una etiqueta de fallo opcional. Si la expectativa falla, Jasmine agrega esta etiqueta al mensaje de fallo de expectativa. En una spec con múltiples expectativas, puede ayudar a aclarar qué salió mal y qué expectativa falló.

Las pruebas restantes confirman la lógica del componente cuando el servicio retorna diferentes valores. La segunda prueba valida el efecto de cambiar el nombre del usuario. La tercera prueba verifica que el componente muestra el mensaje apropiado cuando no hay usuario autenticado.

Componente con servicio async

En esta muestra, la plantilla AboutComponent aloja un TwainComponent. El TwainComponent muestra citas 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: El valor de la propiedad quote del componente pasa a través de un AsyncPipe. Eso significa que la propiedad retorna ya sea una Promise o un Observable.

En este ejemplo, el método TwainComponent.getQuote() te dice que la propiedad quote retorna un 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      }),    );  }}

El TwainComponent obtiene citas de un TwainService inyectado. El componente inicia el Observable retornado con un valor placeholder ('...'), antes de que el servicio pueda retornar su primera cita.

El catchError intercepta errores de servicio, prepara un mensaje de error, y retorna el valor placeholder en el canal de éxito.

Estas son todas características que querrás probar.

Probar con un spy

Cuando se prueba un componente, solo la API pública del servicio debería importar. En general, las pruebas mismas no deberían hacer llamadas a servidores remotos. Deberían emular tales llamadas. La configuración en este app/twain/twain.component.spec.ts muestra una forma de hacerlo:

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('...');    }));  });});

Enfócate en el 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('...');    }));  });});

El spy está diseñado de tal manera que cualquier llamada a getQuote recibe un observable con una cita de prueba. A diferencia del método getQuote() real, este spy evita el servidor y retorna un observable síncrono cuyo valor está disponible inmediatamente.

Puedes escribir muchas pruebas útiles con este spy, aunque su Observable sea síncrono.

ÚTIL: Es mejor limitar el uso de spies solo a lo que es necesario para la prueba. Crear mocks o spies para más de lo necesario puede ser frágil. A medida que el componente e injectable evolucionan, las pruebas no relacionadas pueden fallar porque ya no simulan suficientes comportamientos que de otro modo no afectarían la prueba.

Prueba async con fakeAsync()

Para usar la funcionalidad fakeAsync(), debes importar zone.js/testing en tu archivo de configuración de pruebas. Si creaste tu proyecto con Angular CLI, zone-testing ya está importado en src/test.ts.

La siguiente prueba confirma el comportamiento esperado cuando el servicio retorna un 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: La función it() recibe un argumento de la siguiente forma.

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

La función fakeAsync() habilita un estilo de codificación lineal ejecutando el cuerpo de la prueba en una fakeAsync test zone especial. El cuerpo de la prueba aparece ser síncrono. No hay sintaxis anidada (como un Promise.then()) para interrumpir el flujo de control.

ÚTIL: Limitación: La función fakeAsync() no funcionará si el cuerpo de la prueba hace una llamada XMLHttpRequest (XHR). Las llamadas XHR dentro de una prueba son raras, pero si necesitas llamar XHR, consulta la sección waitForAsync().

IMPORTANTE: Ten en cuenta que las tareas asíncronas que ocurren dentro de la zona fakeAsync necesitan ejecutarse manualmente con flush o tick. Si intentas esperar a que se completen (es decir, usando fixture.whenStable) sin usar los helpers de prueba fakeAsync para avanzar el tiempo, tu prueba probablemente fallará. Ver abajo para más información.

La función tick()

Tienes que llamar a tick() para avanzar el reloj virtual.

Llamar a tick() simula el paso del tiempo hasta que todas las actividades asíncronas pendientes terminen. En este caso, espera al setTimeout() del observable.

La función tick() acepta millis y tickOptions como parámetros. El parámetro millis especifica cuánto avanza el reloj virtual y por defecto es 0 si no se proporciona. Por ejemplo, si tienes un setTimeout(fn, 100) en una prueba fakeAsync(), necesitas usar tick(100) para desencadenar el callback fn. El parámetro opcional tickOptions tiene una propiedad llamada processNewMacroTasksSynchronously. La propiedad processNewMacroTasksSynchronously representa si invocar nuevas macro tasks generadas al hacer tick y por defecto es 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);      });    }));  });});

La función tick() es una de las utilidades de testing de Angular que importas con TestBed. Es una compañera de fakeAsync() y solo puedes llamarla dentro de un cuerpo fakeAsync().

tickOptions

En este ejemplo, tienes una nueva macro task, la función setTimeout anidada. Por defecto, cuando el tick es setTimeout, tanto outside como nested se desencadenarán.

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);      });    }));  });});

En algunos casos, no quieres desencadenar la nueva macro task al hacer tick. Puedes usar tick(millis, {processNewMacroTasksSynchronously: false}) para no invocar una nueva 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);      });    }));  });});

Comparar fechas dentro de fakeAsync()

fakeAsync() simula el paso del tiempo, lo que te permite calcular la diferencia entre fechas 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 con fakeAsync()

Jasmine también proporciona una característica clock para simular fechas. Angular ejecuta automáticamente pruebas que se ejecutan después de que se llame jasmine.clock().install() dentro de un método fakeAsync() hasta que se llame jasmine.clock().uninstall(). fakeAsync() no es necesario y lanza un error si se anida.

Por defecto, esta característica está deshabilitada. Para habilitarla, establece una bandera global antes de importar zone-testing.

Si usas Angular CLI, configura esta bandera en 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);      });    }));  });});

Usar el scheduler de RxJS dentro de fakeAsync()

También puedes usar el scheduler de RxJS en fakeAsync() igual que usando setTimeout() o setInterval(), pero necesitas importar zone.js/plugins/zone-patch-rxjs-fake-async para parchear el scheduler de 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);      });    }));  });});

Soportar más macroTasks

Por defecto, fakeAsync() soporta las siguientes macro tasks.

  • setTimeout
  • setInterval
  • requestAnimationFrame
  • webkitRequestAnimationFrame
  • mozRequestAnimationFrame

Si ejecutas otras macro tasks como HTMLCanvasElement.toBlob(), se lanza un error "Unknown macroTask scheduled in fake async test".

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;    });  }}

Si quieres soportar tal caso, necesitas definir la macro task que quieres soportar en beforeEach(). Por ejemplo:

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 hacer que el elemento <canvas> sea Zone.js-aware en tu aplicación, necesitas importar el parche zone-patch-canvas (ya sea en polyfills.ts o en el archivo 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 async

Podrías estar satisfecho con la cobertura de prueba de estas pruebas.

Sin embargo, podrías estar preocupado por el hecho de que el servicio real no se comporta exactamente de esta manera. El servicio real envía solicitudes a un servidor remoto. Un servidor toma tiempo en responder y la respuesta ciertamente no estará disponible inmediatamente como en las dos pruebas anteriores.

Tus pruebas reflejarán el mundo real más fielmente si retornas un observable asíncrono del spy getQuote() así.

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 async

El observable async fue producido por un helper asyncData. El helper asyncData es una función utilitaria que tendrás que escribir tú mismo, o copiar esta de el código de muestra.

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));}

El observable de este helper emite el valor data en el siguiente turno del motor de JavaScript.

El operador defer() de RxJS retorna un observable. Toma una función factory que retorna ya sea una promesa o un observable. Cuando algo se suscribe al observable de defer, agrega el suscriptor a un nuevo observable creado con esa factory.

El operador defer() transforma el Promise.resolve() en un nuevo observable que, como HttpClient, emite una vez y se completa. Los suscriptores se desuscriben después de recibir el valor de datos.

Hay un helper similar para producir un error async.

/* * 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));}

Más pruebas async

Ahora que el spy getQuote() está retornando observables async, la mayoría de tus pruebas tendrán que ser async también.

Aquí hay una prueba fakeAsync() que demuestra el flujo de datos que esperarías en el 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('...');    }));  });});

Nota que el elemento quote muestra el valor placeholder ('...') después de ngOnInit(). La primera cita aún no ha llegado.

Para vaciar la primera cita del observable, llamas a tick(). Luego llamas a detectChanges() para decirle a Angular que actualice la pantalla.

Entonces puedes afirmar que el elemento quote muestra el texto esperado.

Prueba async sin fakeAsync()

Aquí está la prueba fakeAsync() anterior, reescrita con el 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

La prueba debe esperar a que el observable getQuote() emita la siguiente cita. En lugar de llamar a tick(), llama a fixture.whenStable().

El fixture.whenStable() retorna una promesa que se resuelve cuando la cola de tareas del motor de JavaScript se vuelve vacía. En este ejemplo, la cola de tareas se vuelve vacía cuando el observable emite la primera cita.

Componente con inputs y outputs

Un componente con inputs y outputs típicamente aparece dentro de la plantilla de vista de un componente host. El host usa un property binding para establecer la propiedad input y un event binding para escuchar eventos generados por la propiedad output.

El objetivo de testing es verificar que tales bindings funcionen como se espera. Las pruebas deberían establecer valores de input y escuchar eventos de output.

El DashboardHeroComponent es un pequeño ejemplo de un componente en este rol. Muestra un héroe individual proporcionado por el DashboardComponent. Hacer clic en ese héroe le dice al DashboardComponent que el usuario ha seleccionado el héroe.

El DashboardHeroComponent está incrustado en la plantilla DashboardComponent así:

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>

El DashboardHeroComponent aparece en un bloque @for, que establece la propiedad input hero de cada componente al valor de bucle y escucha el evento selected del componente.

Aquí está la definición completa del componente:

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());  }}

Aunque probar un componente tan simple tiene poco valor intrínseco, vale la pena saber cómo. Usa uno de estos enfoques:

  • Probarlo como se usa por DashboardComponent
  • Probarlo como un componente standalone
  • Probarlo como se usa por un sustituto para DashboardComponent

El objetivo inmediato es probar el DashboardHeroComponent, no el DashboardComponent, así que, prueba la segunda y tercera opciones.

Probar DashboardHeroComponent standalone

Aquí está la parte principal de la configuración del archivo 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;  }}

Nota cómo el código de configuración asigna un héroe de prueba (expectedHero) a la propiedad hero del componente, emulando la forma en que el DashboardComponent lo establecería usando el property binding en su repeater.

La siguiente prueba verifica que el nombre del héroe se propaga a la plantilla usando un 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;  }}

Porque la plantilla pasa el nombre del héroe a través del UpperCasePipe de Angular, la prueba debe coincidir el valor del elemento con el nombre en mayúsculas.

Hacer clic

Hacer clic en el héroe debería generar un evento selected que el componente host (DashboardComponent presumiblemente) puede escuchar:

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;  }}

La propiedad selected del componente retorna un EventEmitter, que se ve como un Observable síncrono de RxJS para los consumidores. La prueba se suscribe a él explícitamente igual que el componente host lo hace implícitamente.

Si el componente se comporta como se espera, hacer clic en el elemento del héroe debería decirle a la propiedad selected del componente que emita el objeto hero.

La prueba detecta ese evento a través de su suscripción a selected.

triggerEventHandler

El heroDe en la prueba anterior es un DebugElement que representa el <div> del héroe.

Tiene propiedades y métodos de Angular que abstraen la interacción con el elemento nativo. Esta prueba llama al DebugElement.triggerEventHandler con el nombre de evento "click". El binding de evento "click" responde llamando a DashboardHeroComponent.click().

El DebugElement.triggerEventHandler de Angular puede generar cualquier evento vinculado a datos por su nombre de evento. El segundo parámetro es el objeto de evento pasado al handler.

La prueba desencadenó un 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;  }}

En este caso, la prueba asume correctamente que el handler de evento de runtime, el método click() del componente, no se preocupa por el objeto de evento.

ÚTIL: Otros handlers son menos indulgentes. Por ejemplo, la directiva RouterLink espera un objeto con una propiedad button que identifica qué botón del mouse, si alguno, fue presionado durante el clic. La directiva RouterLink lanza un error si el objeto de evento falta.

Hacer clic en el elemento

La siguiente alternativa de prueba llama al método click() propio del elemento nativo, que es perfectamente fino para este componente.

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()

Hacer clic en un botón, un anchor, o un elemento HTML arbitrario es una tarea de prueba común.

Haz eso consistente y directo encapsulando el proceso click-triggering en un helper como la siguiente función 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);  }}

El primer parámetro es el elemento-a-hacer-clic. Si quieres, pasa un objeto de evento personalizado como segundo parámetro. El predeterminado es un objeto de evento de mouse de botón izquierdo parcial aceptado por muchos handlers incluyendo la directiva RouterLink.

IMPORTANTE: La función helper click() no es una de las utilidades de testing de Angular. Es una función definida en el código de muestra de esta guía. Todas las pruebas de muestra la usan. Si te gusta, agrégala a tu propia colección de helpers.

Aquí está la prueba anterior, reescrita usando el 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;  }}

Componente dentro de un test host

Las pruebas anteriores jugaron el rol del DashboardComponent host ellas mismas. ¿Pero funciona correctamente el DashboardHeroComponent cuando está correctamente vinculado a datos a un componente 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;  }}

El test host establece la propiedad input hero del componente con su héroe de prueba. Vincula el evento selected del componente con su handler onSelected, que registra el héroe emitido en su propiedad selectedHero.

Más tarde, las pruebas podrán verificar selectedHero para verificar que el evento DashboardHeroComponent.selected emitió el héroe esperado.

La configuración para las pruebas test-host es similar a la configuración para las pruebas standalone:

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 configuración de módulo de testing muestra dos diferencias importantes:

  • Crea el TestHostComponent en lugar del DashboardHeroComponent
  • El TestHostComponent establece el DashboardHeroComponent.hero con un binding

El createComponent retorna un fixture que contiene una instancia de TestHostComponent en lugar de una instancia de DashboardHeroComponent.

Crear el TestHostComponent tiene el efecto secundario de crear un DashboardHeroComponent porque este último aparece dentro de la plantilla del primero. La consulta para el elemento héroe (heroEl) aún lo encuentra en el DOM de prueba, aunque a mayor profundidad en el árbol de elementos que antes.

Las pruebas mismas son casi idénticas a la versión standalone:

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;  }}

Solo la prueba de evento selected difiere. Confirma que el héroe DashboardHeroComponent seleccionado realmente encuentra su camino hacia arriba a través del event binding al componente host.

Componente de enrutamiento

Un componente de enrutamiento es un componente que le dice al Router que navegue a otro componente. El DashboardComponent es un componente de enrutamiento porque el usuario puede navegar al HeroDetailComponent haciendo clic en uno de los botones de héroe en el dashboard.

Angular proporciona helpers de prueba para reducir boilerplate y probar más efectivamente código que depende de HttpClient. La función provideRouter puede usarse directamente en el módulo de prueba también.

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}`);    });  });}

La siguiente prueba hace clic en el héroe mostrado y confirma que navegamos a la 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}`);    });  });}

Componentes enrutados

Un componente enrutado es el destino de una navegación Router. Puede ser más difícil de probar, especialmente cuando la ruta al componente incluye parámetros. El HeroDetailComponent es un componente enrutado que es el destino de tal ruta.

Cuando un usuario hace clic en un héroe de Dashboard, el DashboardComponent le dice al Router que navegue a heroes/:id. El :id es un parámetro de ruta cuyo valor es el id del héroe a editar.

El Router coincide esa URL con una ruta al HeroDetailComponent. Crea un objeto ActivatedRoute con la información de enrutamiento y lo inyecta en una nueva instancia del HeroDetailComponent.

Aquí están los servicios inyectados en 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});  }}

El componente HeroDetail necesita el parámetro id para poder obtener el héroe correspondiente usando el HeroDetailService. El componente tiene que obtener el id de la propiedad ActivatedRoute.paramMap que es un Observable.

No puede simplemente referenciar la propiedad id del ActivatedRoute.paramMap. El componente tiene que suscribirse al observable ActivatedRoute.paramMap y estar preparado para que el id cambie durante su vida útil.

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});  }}

Las pruebas pueden explorar cómo el HeroDetailComponent responde a diferentes valores de parámetro id navegando a diferentes rutas.

Pruebas de componentes anidados

Las plantillas de componentes a menudo tienen componentes anidados, cuyas plantillas podrían contener más componentes.

El árbol de componentes puede ser muy profundo y a veces los componentes anidados no juegan ningún rol en probar el componente en la parte superior del árbol.

El AppComponent, por ejemplo, muestra una barra de navegación con anchors y sus directivas RouterLink.

app/app.component.html

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

Para validar los enlaces pero no la navegación, no necesitas el Router para navegar y no necesitas el <router-outlet> para marcar dónde el Router inserta componentes enrutados.

El BannerComponent y WelcomeComponent (indicados por <app-banner> y <app-welcome>) también son irrelevantes.

Sin embargo, cualquier prueba que cree el AppComponent en el DOM también crea instancias de estos tres componentes y, si dejas que eso suceda, tendrás que configurar el TestBed para crearlos.

Si descuidas declararlos, el compilador de Angular no reconocerá las etiquetas <app-banner>, <app-welcome> y <router-outlet> en la plantilla AppComponent y lanzará un error.

Si declaras los componentes reales, también tendrás que declarar sus componentes anidados y proporcionar todos los servicios inyectados en cualquier componente en el árbol.

Esta sección describe dos técnicas para minimizar la configuración. Úsalas, solas o en combinación, para mantener el foco en probar el componente primario.

Hacer stub de componentes innecesarios

En la primera técnica, creas y declaras versiones stub de los componentes y directiva que juegan poco o ningún rol en las pruebas.

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');  }));}

Los selectores stub coinciden con los selectores para los componentes reales correspondientes. Pero sus plantillas y clases están vacías.

Luego declár alos sobrescribiendo los imports de tu componente 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: La clave set en este ejemplo reemplaza todos los imports existentes en tu componente, asegúrate de importar todas las dependencias, no solo los stubs. Alternativamente puedes usar las claves remove/add para remover y agregar imports selectivamente.

NO_ERRORS_SCHEMA

En el segundo enfoque, agrega NO_ERRORS_SCHEMA a los overrides de metadata de tu componente.

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');  }));}

El NO_ERRORS_SCHEMA le dice al compilador de Angular que ignore elementos y atributos no reconocidos.

El compilador reconoce el elemento <app-root> y el atributo routerLink porque declaraste un AppComponent y RouterLink correspondientes en la configuración del TestBed.

Pero el compilador no lanzará un error cuando encuentre <app-banner>, <app-welcome> o <router-outlet>. Simplemente los renderiza como etiquetas vacías y el navegador los ignora.

Ya no necesitas los componentes stub.

Usar ambas técnicas juntas

Estas son técnicas para Shallow Component Testing, llamadas así porque reducen la superficie visual del componente solo a aquellos elementos en la plantilla del componente que importan para las pruebas.

El enfoque NO_ERRORS_SCHEMA es el más fácil de los dos pero no lo uses en exceso.

El NO_ERRORS_SCHEMA también evita que el compilador te diga sobre los componentes y atributos faltantes que omitiste inadvertidamente o escribiste mal. Podrías desperdiciar horas persiguiendo bugs fantasma que el compilador habría capturado en un instante.

El enfoque de componente stub tiene otra ventaja. Aunque los stubs en este ejemplo están vacíos, podrías darles plantillas y clases reducidas si tus pruebas necesitan interactuar con ellos de alguna manera.

En la práctica combinarás las dos técnicas en la misma configuración, como se ve en este ejemplo.

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');  }));}

El compilador de Angular crea el BannerStubComponent para el elemento <app-banner> y aplica el RouterLink a los anchors con el atributo routerLink, pero ignora las etiquetas <app-welcome> y <router-outlet>.

By.directive y directivas inyectadas

Un poco más de configuración desencadena el binding de datos inicial y obtiene referencias a los enlaces de navegación:

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');  }));}

Tres puntos de interés especial:

  • Localizar los elementos anchor con una directiva adjunta usando By.directive
  • La consulta retorna wrappers DebugElement alrededor de los elementos coincidentes
  • Cada DebugElement expone un inyector de dependencias con la instancia específica de la directiva adjunta a ese elemento

Los enlaces del AppComponent a validar son los siguientes:

app/app.component.html (navigation links)

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

Aquí hay algunas pruebas que confirman que esos enlaces están conectados a las directivas routerLink como se espera:

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 un objeto page

El HeroDetailComponent es una vista simple con un título, dos campos de héroe y dos botones.

Pero hay bastante complejidad de plantilla incluso en este formulario simple.

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>}

Las pruebas que ejercitan el componente necesitan…

  • Esperar hasta que un héroe llegue antes de que los elementos aparezcan en el DOM
  • Una referencia al texto del título
  • Una referencia al cuadro de entrada de nombre para inspeccionarlo y establecerlo
  • Referencias a los dos botones para poder hacer clic en ellos

Incluso un formulario pequeño como este puede producir un lío de configuración condicional tortuosa y selección de elementos CSS.

Domestica la complejidad con una clase Page que maneja el acceso a las propiedades del componente y encapsula la lógica que las establece.

Aquí hay tal clase Page para el 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[];  }}

Ahora los hooks importantes para manipulación e inspección de componentes están organizados ordenadamente y son accesibles desde una instancia de Page.

Un método createComponent crea un objeto page y llena los espacios en blanco una vez que el hero llega.

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[];  }}

Aquí hay algunas pruebas más de HeroDetailComponent para reforzar el punto.

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 component providers

El HeroDetailComponent proporciona su propio 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});  }}

No es posible hacer stub del HeroDetailService del componente en los providers del TestBed.configureTestingModule. Esos son providers para el módulo de testing, no el componente. Preparan el inyector de dependencias en el nivel del fixture.

Angular crea el componente con su propio inyector, que es un hijo del inyector del fixture. Registra los providers del componente (el HeroDetailService en este caso) con el inyector hijo.

Una prueba no puede llegar a servicios del inyector hijo desde el inyector del fixture. Y TestBed.configureTestingModule tampoco puede configurarlos.

¡Angular ha creado nuevas instancias del HeroDetailService real todo el tiempo!

ÚTIL: Estas pruebas podrían fallar o agotar el tiempo de espera si el HeroDetailService hiciera sus propias llamadas XHR a un servidor remoto. Podría no haber un servidor remoto al cual llamar.

Afortunadamente, el HeroDetailService delega responsabilidad para acceso a datos remotos a un HeroService inyectado.

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);  }}

La configuración de prueba anterior reemplaza el HeroService real con un TestHeroService que intercepta solicitudes de servidor y falsifica sus respuestas.

¿Qué pasa si no tienes tanta suerte? ¿Qué pasa si falsificar el HeroService es difícil? ¿Qué pasa si HeroDetailService hace sus propias solicitudes de servidor?

El método TestBed.overrideComponent puede reemplazar los providers del componente con test doubles fáciles de manejar como se ve en la siguiente variación de configuración:

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[];  }}

Nota que TestBed.configureTestingModule ya no proporciona un HeroService falso porque no es necesario.

El método overrideComponent

Enfócate en el 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[];  }}

Toma dos argumentos: el tipo de componente a sobrescribir (HeroDetailComponent) y un objeto de metadata de override. El objeto de metadata de override es un genérico definido como sigue:

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

Un objeto de metadata override puede agregar-y-remover elementos en propiedades de metadata o restablecer completamente esas propiedades. Este ejemplo restablece los metadata providers del componente.

El parámetro de tipo, T, es el tipo de metadata que pasarías al decorador @Component:

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

Proporcionar un spy stub (HeroDetailServiceSpy)

Este ejemplo reemplaza completamente el array providers del componente con un nuevo array que contiene un HeroDetailServiceSpy.

El HeroDetailServiceSpy es una versión stubbed del HeroDetailService real que falsifica todas las características necesarias de ese servicio. No inyecta ni delega al HeroService de nivel inferior, por lo que no hay necesidad de proporcionar un test double para eso.

Las pruebas relacionadas con HeroDetailComponent afirmarán que los métodos del HeroDetailService fueron llamados espiando en los métodos del servicio. En consecuencia, el stub implementa sus 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[];  }}

Las pruebas override

Ahora las pruebas pueden controlar el héroe del componente directamente manipulando el testHero del spy-stub y confirmar que los métodos del servicio fueron llamados.

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[];  }}

Más overrides

El método TestBed.overrideComponent puede llamarse múltiples veces para los mismos o diferentes componentes. El TestBed ofrece métodos similares overrideDirective, overrideModule y overridePipe para profundizar y reemplazar partes de estas otras clases.

Explora las opciones y combinaciones por tu cuenta.