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

Para ver la aplicación de muestra que describen las guías de prueba, consulte la aplicación de muestra.

Para conocer las características de las pruebas en las guías de prueba, consulte pruebas.

Enlace de componentes

En la aplicación de ejemplo, 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 vinculando al componente title propiedad como esta.

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

Por mínimo que sea, decide agregar una prueba para confirmar que el componente realmente muestra el contenido correcto donde cree que debería.

Consulta para el


Escribirás una secuencia de pruebas que inspeccionen el valor de la <h1> elemento que envuelve el título enlace de interpolación de propiedad.

Actualizas el beforeEach para encontrar ese elemento con un HTML estándar querySelector y asígnelo al h1 variable.

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

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

createComponent () no vincula datos

Para su primera prueba, le gustaría ver que la pantalla muestra el valor predeterminado title. Su instinto es escribir una prueba que inspeccione inmediatamente el <h1> como esto:

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

Esa prueba falla con el mensaje:

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

La vinculación 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 o se completa una actividad asincrónica (por ejemplo, AJAX).

los TestBed.createComponent lo hace no detección de cambio de gatillo; un hecho confirmado en la prueba revisada:

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

detectChanges ()

Debes decirle al TestBed para realizar el enlace de datos llamando fixture.detectChanges(). Sólo entonces <h1> tener el título esperado.

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

La detección retardada de cambios es intencionada y útil. Le da al probador la oportunidad de inspeccionar y cambiar el estado del componente. antes de que Angular inicie el enlace de datos y llame a los ganchos del ciclo de vida.

Aquí hay otra prueba que cambia el componente title propiedad antes de vocación fixture.detectChanges().

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

los BannerComponent las pruebas llaman con frecuencia detectChanges. Algunos probadores prefieren que el entorno de prueba angular ejecute la detección de cambios automáticamente.

Eso es posible configurando el TestBed con el ComponentFixtureAutoDetect proveedor. Primero impórtelo desde la biblioteca de la utilidad de prueba:

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

Luego agréguelo al providers matriz de la configuración del módulo de prueba:

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

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

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

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

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

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 prueba Angular no no saber que la prueba cambió el componente title. los ComponentFixtureAutoDetect el servicio responde a actividades asincrónicas como resolución de promesas, temporizadores y eventos DOM. Pero una actualización directa y sincrónica de la propiedad del componente es invisible. La prueba debe llamar fixture.detectChanges() manualmente para activar otro ciclo de detección de cambios.

En lugar de preguntarse cuándo el dispositivo de prueba realizará o no la detección de cambios, las muestras de esta guía llamar siempre detectChanges() explícitamente. No hay nada de malo en llamar detectChanges() más a menudo de lo estrictamente necesario.

Cambiar un valor de entrada con dispatchEvent ()

Para simular la entrada del usuario, puede encontrar el elemento de entrada y establecer su value propiedad.

Tu llamarás fixture.detectChanges() para activar la detección de cambios de Angular. Pero hay un paso intermedio esencial.

Angular no sabe que configuraste el elemento de entrada value propiedad. No leerá esa propiedad hasta que aumente el valor del elemento. input evento llamando dispatchEvent(). Luego llama detectChanges().

El siguiente ejemplo demuestra la secuencia correcta.

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

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

  // Dispatch a DOM event so that Angular learns of input value change.
  // In older browsers, such as IE, you might need a CustomEvent instead. See
  // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
  nameInput.dispatchEvent(new Event('input'));

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

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

Componente con archivos externos

los BannerComponent arriba se define con un plantilla en línea y CSS en línea, especificado en el @Component.template y @Component.styles propiedades respectivamente.

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

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

Esta sintaxis le dice al compilador Angular que lea los archivos externos durante la compilación del componente.

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

Sin embargo, si ejecuta las pruebas en un entorno no CLI, las pruebas de este componente pueden fallar. Por ejemplo, si ejecuta el BannerComponent pruebas en un entorno de codificación web como plunker, verá un mensaje como este:

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

Obtiene este mensaje de error de prueba cuando el entorno de ejecución compila el código fuente durante las pruebas mismas.

Para corregir el problema, llame compileComponents() como se explica a continuación.

Componente con dependencia

Los componentes suelen tener dependencias de servicio.

los WelcomeComponent muestra un mensaje de bienvenida al usuario que ha iniciado sesión. Sabe quién es el usuario en función de una propiedad del inyectado UserService:

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

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

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

los WelcomeComponent tiene una lógica de decisión que interactúa con el servicio, lógica que hace que valga la pena probar este componente. Aquí está la configuración del módulo de prueba para el archivo de especificaciones:

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

Esta vez, además de declarar el componente bajo prueba, la configuración agrega un UserService proveedor para el providers lista. Pero no lo real UserService.

Proporcionar dobles de prueba de servicio

A componente bajo prueba no tiene que inyectarse con servicios reales. De hecho, suele ser mejor si son dobles de prueba (talones, falsificaciones, espías o simulacros). El propósito de la especificación es probar el componente, no el servicio, y los servicios reales pueden causar problemas.

Inyectando lo real UserService podría ser una pesadilla. El servicio real puede solicitar al usuario las credenciales de inicio de sesión e intentar acceder a un servidor de autenticación. Estos comportamientos pueden ser difíciles de interceptar. Es mucho más fácil y seguro crear y registrar un doble de prueba en lugar del real. UserService.

Este conjunto de pruebas en particular proporciona una mínima simulación del UserService que satisfaga las necesidades del WelcomeComponent y sus pruebas:

let userServiceStub: Partial<UserService>;

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

Obtenga servicios inyectados

Las pruebas necesitan acceso al (stub) UserService inyectado en el WelcomeComponent.

Angular tiene un sistema de inyección jerárquico. Puede haber inyectores en varios niveles, desde el inyector de 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 en que siempre funciona, Es para obtenerlo del inyector del componente bajo prueba. El inyector de componentes es una propiedad de la luminaria. DebugElement.

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

TestBed.inject ()

usted mayo también podrá obtener el servicio desde el inyector raíz a través de TestBed.inject(). Esto es más fácil de recordar y menos detallado. Pero solo funciona cuando Angular inyecta el componente con la instancia del servicio en el inyector raíz de la prueba.

En este conjunto de pruebas, el solamente proveedor de UserService es el módulo de prueba raíz, por lo que es seguro llamar TestBed.inject() como sigue:

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

Para un caso de uso en el que TestBed.inject() no funciona, vea el Proveedores de componentes de reemplazo sección que explica cuándo y por qué debe obtener el servicio del inyector del componente.

Configuración y pruebas finales

Aquí está el completo beforeEach(), utilizando TestBed.inject():

let userServiceStub: Partial<UserService>;

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

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

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

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

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

Y aquí hay algunas pruebas:

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

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

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

La primera es una prueba de cordura; confirma que el golpeado UserService se llama y funciona.

El segundo parámetro del comparador Jasmine (p. Ej., 'expected name') es una etiqueta de error opcional. Si la expectativa falla, Jasmine agrega esta etiqueta al mensaje de falla de expectativa. En una especificación 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 devuelve valores diferentes. La segunda prueba valida el efecto de cambiar el nombre de usuario. La tercera prueba verifica que el componente muestre el mensaje correcto cuando no hay ningún usuario que haya iniciado sesión.

Componente con servicio asincrónico

En esta muestra, el AboutComponent plantilla aloja un TwainComponent. los TwainComponent muestra citas de Mark Twain.

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

Tenga en cuenta que el valor de los componentes quote la propiedad pasa por un AsyncPipe. Eso significa que la propiedad devuelve un Promise o un Observable.

En este ejemplo, el TwainComponent.getQuote() El método te dice que el quote propiedad devuelve un Observable.

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

los TwainComponent obtiene cotizaciones de un inyectado TwainService. El componente comienza el devuelto Observable con un valor de marcador de posición'...'), antes de que el servicio pueda devolver su primer presupuesto.

los catchError intercepta errores de servicio, prepara un mensaje de error y devuelve el valor de marcador de posición en el canal de éxito. Debe esperar un tic para configurar el errorMessage para evitar actualizar ese mensaje dos veces en el mismo ciclo de detección de cambios.

Estas son todas las funciones que querrá probar.

Prueba con un espía

Al probar un componente, solo debería importar la API pública del servicio. En general, las pruebas en sí mismas no deben realizar 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:

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

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

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

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

Concéntrate en el espía.

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

El espía está diseñado de tal manera que cualquier llamada a getQuote recibe un observable con una cotización de prueba. A diferencia de lo real getQuote() método, este espía pasa por alto el servidor y devuelve un observable sincrónico cuyo valor está disponible inmediatamente.

Puede escribir muchas pruebas útiles con este espía, aunque su Observable es sincrónico.

Pruebas sincrónicas

Una ventaja clave de un síncrono Observable es que a menudo puede convertir procesos asincrónicos en pruebas sincrónicas.

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

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

Debido a que el resultado del espía regresa sincrónicamente, el getQuote() El método actualiza el mensaje en la pantalla inmediatamente. después el primer ciclo de detección de cambios durante el cual Angular llama ngOnInit.

No tiene tanta suerte al probar la ruta del error. Aunque el servicio espía devolverá un error de forma sincrónica, el método del componente llama setTimeout(). La prueba debe esperar al menos una vuelta completa del motor JavaScript antes de que el valor esté disponible. La prueba debe convertirse asincrónico.

Prueba asíncrona con fakeAsync ()

Usar fakeAsync() funcionalidad, debe importar zone.js/testing en su archivo de configuración de prueba. Si creó su proyecto con el CLI angular, zone-testing ya está importado en src/test.ts.

La siguiente prueba confirma el comportamiento esperado cuando el servicio devuelve un ErrorObservable.

it('should display error when TwainService fails', fakeAsync(() => {
     // tell spy to return an error observable
     getQuoteSpy.and.returnValue(throwError('TwainService test failure'));

     fixture.detectChanges();  // onInit()
     // sync spy errors immediately after init

     tick();  // flush the component's setTimeout()

     fixture.detectChanges();  // update errorMessage within setTimeout()

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

Tenga en cuenta que el it() La función recibe un argumento de la siguiente forma.

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

los fakeAsync() La función permite un estilo de codificación lineal ejecutando el cuerpo de prueba en un fakeAsync test zone. El cuerpo de prueba parece estar sincronizado. No hay sintaxis anidada (como un Promise.then()) para interrumpir el flujo de control.

Limitación: La fakeAsync() La función no funcionará si el cuerpo de prueba hace un XMLHttpRequest (XHR) llamada. Las llamadas XHR dentro de una prueba son raras, pero si necesita llamar a XHR, consulte waitForAsync(), debajo.

los garrapata() función

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

Llamar a tick () simula el paso del tiempo hasta que finalizan todas las actividades asincrónicas pendientes. En este caso, espera a que el controlador de errores setTimeout().

La función tick () acepta milisegundos y tickOptions como parámetros, el parámetro milisegundo (predeterminado en 0 si no se proporciona) representa cuánto avanza el reloj virtual. Por ejemplo, si tiene un setTimeout(fn, 100) en un fakeAsync() prueba, debe usar tick (100) para activar la devolución de llamada fn. TickOptions es un parámetro opcional con una propiedad llamada processNewMacroTasksSynchronously (predeterminado en verdadero) que representa si se invocan nuevas tareas de macro generadas al marcar.

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

La función tick () es una de las utilidades de prueba angular que importa con TestBed. Es un compañero de fakeAsync() y solo puedes llamarlo dentro de un fakeAsync() cuerpo.

tickOptions

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

En este ejemplo, tenemos una nueva macro tarea (setTimeout anidado), por defecto, cuando tick, el setTimeout outside y nested ambos se activarán.

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

Y en algunos casos, no queremos activar la nueva macro tarea al marcar, podemos usar tick(milliseconds, {processNewMacroTasksSynchronously: false}) para no invocar una nueva macro tarea.

Comparación de fechas dentro de fakeAsync ()

fakeAsync() simula el paso del tiempo, lo que le permite calcular la diferencia entre fechas dentro fakeAsync().

it('should get Date diff correctly in fakeAsync', fakeAsync(() => {
     const start = Date.now();
     tick(100);
     const end = Date.now();
     expect(end - start).toBe(100);
   }));

jasmine.clock con fakeAsync ()

Jasmine también proporciona un clock función para simular fechas. Angular ejecuta automáticamente las pruebas que se ejecutan después jasmine.clock().install() se llama dentro de un fakeAsync() método hasta jasmine.clock().uninstall() se llama. fakeAsync() no es necesario y arroja un error si está anidado.

De forma predeterminada, esta función está desactivada. Para habilitarlo, establezca una bandera global antes de importar zone-testing.

Si usa la CLI angular, configure esta bandera en src/test.ts.

(window as any)['__zone_symbol__fakeAsyncPatchLock'] = true;
import 'zone.js/testing';
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);
  });
});

Usando el programador RxJS dentro de fakeAsync ()

También puede usar el programador RxJS en fakeAsync() como usar setTimeout() o setInterval(), pero necesitas importar zone.js/plugins/zone-patch-rxjs-fake-async para parchear el programador RxJS.

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 = null;
     of('hello').pipe(delay(1000)).subscribe(v => {
       result = v;
     });
     expect(result).toBeNull();
     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);
   }));

Soporta más macroTasks

Por defecto, fakeAsync() admite las siguientes tareas macro.

  • setTimeout
  • setInterval
  • requestAnimationFrame
  • webkitRequestAnimationFrame
  • mozRequestAnimationFrame

Si ejecuta otras tareas macro como HTMLCanvasElement.toBlob(), un “MacroTask desconocida programada en una prueba asíncrona falsa” se lanzará un error.

import { fakeAsync, TestBed, tick } from '@angular/core/testing';

import { CanvasComponent } from './canvas.component';

describe('CanvasComponent', () => {
  beforeEach(async () => {
    await TestBed
        .configureTestingModule({
          declarations: [CanvasComponent],
        })
        .compileComponents();
  });

  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);
     }));
});
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.fill;
    context.fillRect(0, 0, 200, 200);

    canvas.toBlob(blob => {
      this.blobSize = blob.size;
    });
  }
}

If you want to support such a case, you need to define the macro task you want to support in beforeEach(). Por ejemplo:

beforeEach(() => {
  (window as any).__zone_symbol__FakeAsyncTestMacroTask = [
    {
      source: 'HTMLCanvasElement.toBlob',
      callbackArgs: [{size: 200}],
    },
  ];
});

Tenga en cuenta que para hacer el <canvas> elemento Zone.js en su aplicación, debe importar el zone-patch-canvas parche (ya sea en polyfills.ts o en el archivo específico que usa <canvas>):

// 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';

Observables asíncronos

Es posible que esté satisfecho con la cobertura de prueba de estas pruebas.

Sin embargo, es posible que le preocupe el hecho de que el servicio real no se comporte de esta manera. El servicio real envía solicitudes a un servidor remoto. Un servidor tarda en responder y la respuesta ciertamente no estará disponible de inmediato como en las dos pruebas anteriores.

Sus pruebas reflejarán el mundo real con mayor fidelidad si devuelve un asincrónico observable desde el getQuote() espiar así.

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

Ayudantes observables asíncronos

El observable asincrónico fue producido por un asyncData ayudante. los asyncData helper es una función de utilidad que tendrá que escribir usted mismo, o puede copiar esta del código de muestra.

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

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

los RxJS defer() operador devuelve un observable. Se necesita una función de fábrica que devuelva una promesa o un observable. Cuando algo se suscribe a aplazares observable, agrega el suscriptor a un nuevo observable creado con esa fábrica.

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

Hay un ayudante similar para producir un error asincrónico.

/**
 * 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 asíncronas

Ahora que el getQuote() spy está devolviendo observables asíncronos, la mayoría de las pruebas también tendrán que ser asíncronas.

Aquí está un fakeAsync() prueba que demuestra el flujo de datos que esperaría en el mundo real.

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

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

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

Observe que el elemento de cotización muestra el valor del marcador de posición ('...') después ngOnInit(). La primera cotización aún no ha llegado.

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

Luego, puede afirmar que el elemento de cotización muestra el texto esperado.

Prueba asíncrona con waitForAsync ()

Usar waitForAsync() funcionalidad, debe importar zone.js/testing en su archivo de configuración de prueba. Si creó su proyecto con Angular CLI, zone-testing ya está importado en src/test.ts.

Aquí está el anterior fakeAsync() prueba, reescrito con el waitForAsync() utilidad.

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

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

los waitForAsync() La utilidad oculta un texto repetitivo asincrónico al hacer arreglos para que el código del probador se ejecute en un zona de prueba asíncrona. No necesitas pasar Jasmine’s done() en la prueba y llame done() porque es undefined en devoluciones de llamada prometedoras o observables.

Pero la naturaleza asincrónica de la prueba se revela mediante la llamada a fixture.whenStable(), que rompe el flujo lineal de control.

Cuando se usa un intervalTimer() tal como setInterval() en waitForAsync(), recuerda cancelar el temporizador con clearInterval() después de la prueba, de lo contrario el waitForAsync() nunca termina.

whenStable

La prueba debe esperar al getQuote() observable para emitir la siguiente cita. En lugar de llamar a tick (), llama fixture.whenStable().

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

La prueba se reanuda dentro de la devolución de llamada de promesa, que llama detectChanges() para actualizar el elemento de cotización con el texto esperado.

Jazmín hecho()

Mientras que la waitForAsync() y fakeAsync() Las funciones simplifican enormemente las pruebas asíncronas angulares, aún puede recurrir a la técnica tradicional y aprobar it una función que toma un done llamar de vuelta.

No puedes llamar done() en waitForAsync() o fakeAsync() funciones, porque el done parameter es undefined.

Ahora eres responsable de encadenar promesas, manejar errores y llamar done() en los momentos oportunos.

Escribir funciones de prueba con done(), es más engorroso que waitForAsync()y fakeAsync(), pero ocasionalmente es necesario cuando el código involucra el intervalTimer() igual que setInterval.

Aquí hay dos versiones más de la prueba anterior, escritas con done(). El primero se suscribe al Observable expuesto a la plantilla por el componente quote propiedad.

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

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

El RxJS last() El operador emite el último valor del observable antes de completar, que será la cotización de la prueba. los subscribe llamadas de devolución de llamada detectChanges() para actualizar el elemento de cotización con la cotización de prueba, de la misma manera que las pruebas anteriores.

En algunas pruebas, está más interesado en cómo se llamó a un método de servicio inyectado y qué valores devolvió, que en lo que aparece en la pantalla.

Un espía de servicio, como el qetQuote() espía del falso TwainService, puede brindarle esa información y hacer afirmaciones sobre el estado de la vista.

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

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

Ensayos de componentes de mármol

El anterior TwainComponent Las pruebas simularon una respuesta observable asincrónica de la TwainService con el asyncData y asyncError utilidades.

Estas son funciones breves y simples que puede escribir usted mismo. Desafortunadamente, son demasiado simples para muchos escenarios comunes. Un observable a menudo emite varias veces, quizás después de un retraso significativo. Un componente puede coordinar múltiples observables con secuencias superpuestas de valores y errores.

Prueba de mármol RxJS es una excelente manera de probar escenarios observables, tanto simples como complejos. Probablemente hayas visto el diagramas de mármol que ilustran cómo funcionan los observables. Las pruebas de mármol utilizan un lenguaje de mármol similar para especificar las corrientes observables y las expectativas en sus pruebas.

Los siguientes ejemplos revisitan dos de los TwainComponent pruebas con pruebas de mármol.

Empiece por instalar el jasmine-marbles paquete npm. Luego, importe los símbolos que necesite.

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

Aquí está la prueba completa para obtener una cotización:

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

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

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

  fixture.detectChanges(); // update view

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

Observe que la prueba de Jasmine es sincrónica. No hay fakeAsync(). Las pruebas de Marble utilizan un programador de pruebas para simular el paso del tiempo en una prueba sincrónica.

La belleza de las pruebas de mármol está en la definición visual de las corrientes observables. Esta prueba define un frío observable que espera tres fotogramas (---), emite un valor (x) y completa (|). En el segundo argumento, asigna el marcador de valor (x) al valor emitido (testQuote).

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

La biblioteca de mármol construye el observable correspondiente, que la prueba establece como el getQuote valor de retorno del espía.

Cuando esté listo para activar los observables de mármol, dígale al TestScheduler para enjuagar su cola de tareas preparadas como esta.

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

Este paso tiene un propósito análogo a marcar () y whenStable() en el anterior fakeAsync() y waitForAsync() ejemplos. El saldo de la prueba es el mismo que en esos ejemplos.

Prueba de error de mármol

Aquí está la versión de prueba de mármol del getQuote() prueba de error.

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

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

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

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

Sigue siendo una prueba asincrónica, llamando fakeAsync() y tick (), porque el propio componente llama setTimeout() al procesar errores.

Mira la definición observable de mármol.

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

Esto es un frío observable que espera tres fotogramas y luego emite un error, el hash (#) indica el momento del error que se especifica en el tercer argumento. El segundo argumento es nulo porque el observable nunca emite un valor.

Más información sobre las pruebas de mármol

A marco de mármol es una unidad virtual de tiempo de prueba. Cada símbolo (-, x, |, #) marca el paso de un fotograma.

A frío observable no produce valores hasta que se suscribe. La mayoría de los observables de su aplicación son fríos. Todos HttpClient los métodos vuelven fríos observables.

A caliente observable ya está produciendo valores antes de te suscribes a él. los Router.events observable, que informa la actividad del enrutador, es un caliente observable.

Las pruebas de mármol RxJS son un tema rico, más allá del alcance de esta guía. Obtenga más información en la web, comenzando con el documentación oficial.

Componente con entradas y salidas

Un componente con entradas y salidas suele aparecer dentro de la plantilla de vista de un componente del host. El host usa un enlace de propiedad para establecer la propiedad de entrada y un enlace de evento para escuchar los eventos generados por la propiedad de salida.

El objetivo de la prueba es verificar que dichos enlaces funcionen como se esperaba. Las pruebas deben establecer valores de entrada y escuchar eventos de salida.

los 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 al héroe.

los DashboardHeroComponent está incrustado en el DashboardComponent plantilla como esta:

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

los DashboardHeroComponent aparece en un *ngFor repetidor, que establece cada componente hero propiedad de entrada al valor de bucle y escucha el componente selected evento.

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

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

Si bien probar un componente así de simple tiene poco valor intrínseco, vale la pena saber cómo hacerlo. Puede utilizar uno de estos enfoques:

  • Pruébelo como lo usa DashboardComponent.
  • Pruébelo como un componente independiente.
  • Pruébelo tal como lo utiliza un sustituto de DashboardComponent.

Un vistazo rápido al DashboardComponent constructor desaconseja el primer enfoque:

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

los DashboardComponent depende del enrutador angular y el HeroService. Probablemente tenga que reemplazarlos a ambos con dobles de prueba, lo cual es mucho trabajo. El enrutador parece particularmente desafiante.

La discusión a continuación cubre los componentes de prueba que requieren el enrutador.

El objetivo inmediato es probar el DashboardHeroComponent, no la DashboardComponent, entonces, pruebe la segunda y tercera opción.

Prueba DashboardHeroComponent ser único

Aquí está la esencia de la configuración del archivo de especificaciones.

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

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

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

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

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

Tenga en cuenta cómo el código de configuración asigna un héroe de prueba (expectedHero) a la del componente hero propiedad, emulando la forma en que DashboardComponent lo establecería a través de la propiedad vinculante en su repetidor.

La siguiente prueba verifica que el nombre del héroe se propague a la plantilla mediante un enlace.

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

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

Esta pequeña prueba demuestra cómo las pruebas angulares pueden verificar la representación visual de un componente, algo que no es posible con las pruebas de clase de componentes, a bajo costo y sin recurrir a pruebas de extremo a extremo mucho más lentas y complicadas.

Haciendo clic

Hacer clic en el héroe debería generar un selected evento de que el componente anfitrión (DashboardComponent presumiblemente) puede oír:

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

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

Los componentes selected propiedad devuelve un EventEmitter, que parece un RxJS sincrónico Observable a los consumidores. La prueba se suscribe explícitamente tal como lo hace el componente host implícitamente.

Si el componente se comporta como se esperaba, hacer clic en el elemento del héroe debería indicarle selected propiedad para emitir el hero objeto.

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

triggerEventHandler

los heroDe en la prueba anterior es un DebugElement que representa al héroe <div>.

Tiene propiedades y métodos angulares que abstraen la interacción con el elemento nativo. Esta prueba llama al DebugElement.triggerEventHandler con el nombre del evento “clic”. El enlace de eventos “clic” responde llamando DashboardHeroComponent.click().

El Angular DebugElement.triggerEventHandler puede levantar cualquier evento vinculado a datos por esto nombre del evento. El segundo parámetro es el objeto de evento que se pasa al controlador.

La prueba desencadenó un evento de “clic” con un null objeto de evento.

heroDe.triggerEventHandler('click', null);

La prueba asume (correctamente en este caso) que el controlador de eventos en tiempo de ejecución, el componente click() método: no le importa el objeto de evento.

Otros manipuladores son menos indulgentes. Por ejemplo, el RouterLink directiva espera un objeto con un button propiedad que identifica qué botón del mouse (si lo hay) se presionó durante el clic. los RouterLink La directiva arroja un error si falta el objeto de evento.

Haga clic en el elemento

La siguiente alternativa de prueba llama al propio elemento nativo. click() método, que está perfectamente bien para este componente.

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

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

hacer clic() ayudante

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

Hágalo consistente y fácil encapsulando el activación por clic proceso en un ayudante como el click() función a continuación:

/** 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 clic. Si lo desea, puede pasar un objeto de evento personalizado como segundo parámetro. El valor predeterminado es un (parcial) objeto de evento del botón izquierdo del mouse aceptado por muchos manipuladores, incluido el RouterLink directiva.

los click() la función de ayuda es no una de las utilidades de prueba de Angular. Es una función definida en el código de muestra de esta guía. Todas las pruebas de muestra lo utilizan. Si te gusta, agrégalo a tu propia colección de ayudantes.

Aquí está la prueba anterior, reescrita con el ayudante de clic.

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

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

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

Componente dentro de un host de prueba

Las pruebas anteriores jugaron el papel del anfitrión. DashboardComponent ellos mismos. Pero, ¿el DashboardHeroComponent ¿Funciona correctamente cuando está correctamente vinculado a los datos de un componente del host?

Podrías probar con el DashboardComponent. Pero hacerlo podría requerir mucha configuración, especialmente cuando su plantilla presenta una *ngFor repetidor, otros componentes, diseño HTML, enlaces adicionales, un constructor que inyecta múltiples servicios y comienza a interactuar con esos servicios de inmediato.

Imagínese el esfuerzo para desactivar estas distracciones, solo para demostrar un punto que se puede hacer satisfactoriamente con un anfitrión de prueba como éste:

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

Este host de prueba se une a DashboardHeroComponent como el DashboardComponent pero sin el ruido de la Router, los HeroService, o la *ngFor reloj de repetición.

El host de prueba establece el componente hero propiedad de entrada con su héroe de prueba. Se une a los componentes selected evento con su onSelected controlador, que registra el héroe emitido en su selectedHero propiedad.

Posteriormente, las pruebas podrán comprobar fácilmente selectedHero para verificar que el DashboardHeroComponent.selected evento emitió el héroe esperado.

La configuración para el anfitrión de prueba tests es similar a la configuración de las pruebas independientes:

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

La configuración de este módulo de prueba muestra tres diferencias importantes:

  1. Eso declara ambos DashboardHeroComponent y el TestHostComponent.
  2. Eso crea los TestHostComponent en vez de DashboardHeroComponent.
  3. los TestHostComponent establece el DashboardHeroComponent.hero con una encuadernación.

los createComponent devuelve un fixture que contiene una instancia de TestHostComponent en lugar de una instancia de DashboardHeroComponent.

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

Las pruebas en sí son casi idénticas a la versión independiente:

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

Solo difiere la prueba de evento seleccionada. Confirma que el seleccionado DashboardHeroComponent hero realmente encuentra su camino a través del enlace de eventos al componente de host.

Componente de enrutamiento

A componente de enrutamiento es un componente que le dice al Router para navegar a otro componente. los DashboardComponent es un componente de enrutamiento porque el usuario puede navegar a la HeroDetailComponent haciendo clic en uno de los botones de héroe en el tablero.

El enrutamiento es bastante complicado. Probando el DashboardComponent Parecía abrumador en parte porque involucra la Router, que inyecta junto con el HeroService.

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

Burlándose del HeroService con un espía es una historia familiar. Pero el Router tiene una API complicada y está entrelazada con otros servicios y condiciones previas de la aplicación. ¿Podría ser difícil burlarse?

Afortunadamente, no en este caso porque el DashboardComponent no está haciendo mucho con el Router

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

Este es a menudo el caso de componentes de enrutamiento. Como regla, prueba el componente, no el enrutador, y solo le importa si el componente navega con la dirección correcta en las condiciones dadas.

Proporcionar un enrutador espía para este componente conjunto de pruebas resulta ser tan fácil como proporcionar un HeroService espiar.

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

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

La siguiente prueba hace clic en el héroe mostrado y confirma que Router.navigateByUrl se llama con la URL esperada.

it('should tell ROUTER to navigate when hero clicked', () => {
  heroClick();  // trigger click on first inner <div>

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

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

Componentes enrutados

A componente enrutado es el destino de un Router navegación. Puede ser más complicado de probar, especialmente cuando la ruta al componente incluye parámetros. los HeroDetailComponent es un componente enrutado ese es el destino de dicha ruta.

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

los Router hace coincidir esa URL con una ruta a la HeroDetailComponent. Crea un ActivatedRoute objeto con la información de enrutamiento y lo inyecta en una nueva instancia del HeroDetailComponent.

Aquí esta la HeroDetailComponent constructor:

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

los HeroDetail componente necesita el id parámetro para que pueda obtener el héroe correspondiente a través del HeroDetailService. El componente debe obtener el id desde el ActivatedRoute.paramMap propiedad que es una Observable.

No puede simplemente hacer referencia al id propiedad de la ActivatedRoute.paramMap. El componente tiene que suscribir al ActivatedRoute.paramMap observable y estar preparado para el id cambiar durante su vida.

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

La sección ActivatedRoute en acción del tutorial del enrutador: cubiertas de la guía del recorrido de los héroes ActivatedRoute.paramMap con más detalle.

Las pruebas pueden explorar cómo HeroDetailComponent responde a diferentes id los valores de los parámetros manipulando el ActivatedRoute inyectado en el constructor del componente.

Sabes espiar al Router y un servicio de datos.

Adoptará un enfoque diferente con ActivatedRoute porque

  • paramMap devuelve un Observable que puede emitir más de un valor durante una prueba.
  • Necesita la función de ayuda del enrutador, convertToParamMap(), para crear un ParamMap.
  • Otro componente enrutado las pruebas necesitan un doble de prueba para ActivatedRoute.

Estas diferencias abogan por una clase de código auxiliar reutilizable.

ActivatedRouteStub

El seguimiento ActivatedRouteStub La clase sirve como prueba doble para ActivatedRoute.

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

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

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

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

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

Considere colocar tales ayudantes en un testing carpeta hermano de la app carpeta. Esta muestra pone ActivatedRouteStub en testing/activated-route-stub.ts.

Considere escribir una versión más capaz de esta clase de código auxiliar con el biblioteca de pruebas de mármol.

Probando con ActivatedRouteStub

Aquí hay una prueba que demuestra el comportamiento del componente cuando se observa id se refiere a un héroe existente:

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

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

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

los createComponent() método y page objeto se discuten a continuación. Confíe en su intuición por ahora.

Cuando el id no se puede encontrar, el componente debe volver a enrutar a la HeroListComponent.

La configuración del conjunto de pruebas proporcionó el mismo espía de enrutador descrito anteriormente que espía en el enrutador sin navegar realmente.

Esta prueba espera que el componente intente navegar al HeroListComponent.

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

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

Si bien esta aplicación no tiene una ruta al HeroDetailComponent que omite el id parámetro, podría agregar una ruta de este tipo algún día. El componente debe hacer algo razonable cuando no hay id.

En esta implementación, el componente debe crear y mostrar un nuevo héroe. Los nuevos héroes tienen id=0 y un espacio en blanco name. Esta prueba confirma que el componente se comporta como se esperaba:

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

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

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

Pruebas de componentes anidados

Las plantillas de componentes a menudo tienen componentes anidados, cuyas plantillas pueden contener más componentes.

El árbol de componentes puede ser muy profundo y, la mayoría de las veces, los componentes anidados no juegan ningún papel en la prueba del componente en la parte superior del árbol.

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

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

Mientras que la AppComponent clase está vacío, es posible que desee escribir pruebas unitarias para confirmar que los enlaces están conectados correctamente al RouterLink directivas, tal vez por las razones que se explican a continuación.

Para validar los enlaces, no necesita el Router para navegar y no necesitas el <router-outlet> para marcar donde el Router inserciones componentes enrutados.

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

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

Si no los declara, el compilador Angular no reconocerá los <app-banner>, <app-welcome>, y <router-outlet> etiquetas en el AppComponent plantilla y arrojará un error.

Si declara los componentes reales, también tendrá que declarar su componentes anidados y prever todos servicios inyectados en alguna componente en el árbol.

Eso es demasiado esfuerzo solo para responder algunas preguntas simples sobre enlaces.

Esta sección describe dos técnicas para minimizar la configuración. Úselos, solos o en combinación, para concentrarse en probar el componente principal.

Stubbing de componentes innecesarios

En la primera técnica, crea y declara versiones de código auxiliar de los componentes y la directiva que juegan poco o ningún papel en las pruebas.

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

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

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

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

Luego declararlos en el TestBed configuración junto a los componentes, directivas y tuberías que deben ser reales.

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

los AppComponent es el sujeto de prueba, así que, por supuesto, declaras la versión real.

los RouterLinkDirectiveStub, que se describe más adelante, es una versión de prueba del RouterLink que ayuda con las pruebas de enlace.

El resto son talones.

NO_ERRORS_SCHEMA

En el segundo enfoque, agregue NO_ERRORS_SCHEMA al TestBed.schemas metadatos.

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

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

El compilador reconocerá el <app-root> elemento y el routerLink atributo porque declaró un correspondiente AppComponent y RouterLinkDirectiveStub en el TestBed configuración.

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

Ya no necesita los componentes de talón.

Usa ambas técnicas juntas

Estas son técnicas para Prueba de componentes poco profundos , llamados así porque reducen la superficie visual del componente a solo aquellos elementos en la plantilla del componente que son importantes para las pruebas.

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

los NO_ERRORS_SCHEMA también evita que el compilador le diga acerca de los componentes faltantes y los atributos que omitió inadvertidamente o escribió mal. Podría perder horas persiguiendo errores fantasmas que el compilador habría detectado en un instante.

los componente stub El enfoque tiene otra ventaja. Mientras que los talones en esta ejemplo estaban vacíos, podría darles plantillas y clases reducidas si sus pruebas necesitan interactuar con ellas de alguna manera.

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

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

El compilador Angular crea el BannerComponentStub Para el <app-banner> elemento y aplica el RouterLinkStubDirective a las anclas con el routerLink atributo, pero ignora el <app-welcome> y <router-outlet> etiquetas.

Componentes con RouterLink

El Real RouterLinkDirective es bastante complicado y enredado con otros componentes y directivas de la RouterModule. Requiere una configuración desafiante para simular y usar en pruebas.

los RouterLinkDirectiveStub en este código de muestra reemplaza la directiva real con una versión alternativa diseñada para validar el tipo de cableado de etiqueta de anclaje visto en el AppComponent plantilla.

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

  @HostListener('click')
  onClick() {
    this.navigatedTo = this.linkParams;
  }
}

La URL vinculada al [routerLink] El atributo fluye hacia la directiva linkParams propiedad.

los HostListener cablea el evento de clic del elemento anfitrión (el <a> elementos de anclaje en AppComponent) a la directiva stub onClick método.

Hacer clic en el ancla debería activar el onClick() método, que establece el indicador del stub navigatedTo propiedad. Las pruebas inspeccionan navigatedTo para confirmar que al hacer clic en el ancla se establece la definición de ruta esperada.

Si el enrutador está configurado correctamente para navegar con esa definición de ruta es una cuestión para un conjunto separado de pruebas.

By.directive y directivas inyectadas

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

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

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

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

Tres puntos de especial interés:

  1. Puede localizar los elementos de anclaje con una directiva adjunta utilizando By.directive.

  2. La consulta devuelve DebugElement envoltorios alrededor de los elementos coincidentes.

  3. Cada DebugElement expone un inyector de dependencia con la instancia específica de la directiva adjunta a ese elemento.

los AppComponent Los enlaces para validar son los siguientes:

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

Aquí hay algunas pruebas que confirman que esos enlaces están conectados al routerLink directivas como se esperaba:

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

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

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

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

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

La prueba del “clic” en este ejemplo es engañosa. Prueba el RouterLinkDirectiveStub en lugar del componente. Este es un error común de los stubs de directiva.

Tiene un propósito legítimo en esta guía. Demuestra cómo encontrar un RouterLink elemento, haga clic en él e inspeccione un resultado, sin activar toda la maquinaria del enrutador. Esta es una habilidad que puede necesitar para probar un componente más sofisticado, uno que cambia la pantalla, vuelve a calcular los parámetros o reorganiza las opciones de navegación cuando el usuario hace clic en el enlace.

¿De qué sirven estas pruebas?

Aplastado RouterLink Las pruebas pueden confirmar que un componente con enlaces y una salida está configurado correctamente, que el componente tiene los enlaces que debería tener y que todos apuntan en la dirección esperada. Estas pruebas no se refieren a si la aplicación logrará navegar hasta el componente de destino cuando el usuario haga clic en un enlace.

Stubbing de RouterLink y RouterOutlet es la mejor opción para objetivos de prueba tan limitados. Depender del enrutador real los volvería frágiles. Podrían fallar por razones ajenas al componente. Por ejemplo, un guardia de navegación podría evitar que un usuario no autorizado visite el HeroListComponent. Eso no es culpa del AppComponent y ningún cambio en ese componente podría subsanar la prueba fallida.

A diferente La batería de pruebas puede explorar si la aplicación navega como se espera en presencia de condiciones que influyen en los guardias, como si el usuario está autenticado y autorizado.

Una futura actualización de la guía explicará cómo escribir tales pruebas con el RouterTestingModule.

Utilizar una page objeto

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

HeroDetailComponent en acción

Pero hay mucha complejidad en las plantillas incluso en esta forma simple.

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

Las pruebas que ejercitan el componente necesitan …

  • esperar hasta que llegue un héroe antes de que aparezcan elementos en el DOM.
  • una referencia al texto del título.
  • una referencia al cuadro de entrada de nombre para inspeccionarlo y configurarlo.
  • referencias a los dos botones para que puedan hacer clic en ellos.
  • espías para algunos de los métodos de componentes y enrutadores.

Incluso una forma pequeña como esta puede producir un lío de configuración condicional torturada y selección de elementos CSS.

Domina la complejidad con un Page clase que maneja el acceso a las propiedades de los componentes y encapsula la lógica que los establece.

Aquí hay tal Page clase para el hero-detail.component.spec.ts

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

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

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

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

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

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

Ahora, los ganchos importantes para la manipulación e inspección de componentes están perfectamente organizados y accesibles desde una instancia de Page.

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

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

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

los HeroDetailComponent Las pruebas de una sección anterior demuestran cómo createComponent y page Mantenga las pruebas cortas y en mensaje. No hay distracciones: no hay que esperar a que se resuelvan las promesas ni buscar en el DOM valores de elementos para comparar.

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

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

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

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

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

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

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

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

  // Dispatch a DOM event so that Angular learns of input value change.
  // In older browsers, such as IE, you might need a CustomEvent instead. See
  // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
  nameInput.dispatchEvent(new Event('input'));

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

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

Vocación compileComponents()

Puede ignorar esta sección si solamente ejecutar pruebas con la CLI ng test comando porque la CLI compila la aplicación antes de ejecutar las pruebas.

Si ejecuta pruebas en un entorno no CLI, las pruebas pueden fallar con un mensaje como este:

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

La raíz del problema es que al menos uno de los componentes involucrados en la prueba especifica una plantilla externa o archivo CSS como la siguiente versión del BannerComponent lo hace.

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

La prueba falla cuando el TestBed intenta crear el componente.

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

Recuerde que la aplicación no ha sido compilado. Entonces cuando llamas createComponent(), los TestBed compila implícitamente.

Eso no es un problema cuando el código fuente está en la memoria. Pero el BannerComponent requiere archivos externos que el compilador debe leer del sistema de archivos, un asincrónico operación.

Si el TestBed se les permitió continuar, las pruebas se ejecutarían y fallarían misteriosamente antes de que el compilador pudiera terminar.

El mensaje de error preventivo le dice que compile explícitamente con compileComponents().

compileComponents () es asincrónico

Debes llamar compileComponents() dentro de una función de prueba asincrónica.

Si no hace que la función de prueba sea asincrónica (por ejemplo, olvídese de usar el async palabra clave como se describe a continuación), verá este mensaje de error

Error: ViewDestroyedError: Attempt to use a destroyed view

Un enfoque típico es dividir la lógica de configuración en dos beforeEach() funciones:

  1. Un async beforeEach() que compila los componentes
  2. Un sincrónico beforeEach() que realiza la configuración restante.

El async antes de cada

Escribe el primer async beforeEach como esto.

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

los TestBed.configureTestingModule() método devuelve el TestBed clase para que pueda encadenar llamadas a otros TestBed métodos estáticos como compileComponents().

En este ejemplo, el BannerComponent es el único componente para compilar. Otros ejemplos configuran el módulo de prueba con varios componentes y pueden importar módulos de aplicación que contienen aún más componentes. Cualquiera de ellos podría requerir archivos externos.

los TestBed.compileComponents El método compila de forma asincrónica todos los componentes configurados en el módulo de prueba.

No vuelva a configurar el TestBed después de llamar compileComponents().

Vocación compileComponents() cierra la corriente TestBed instancia para una mayor configuración. No puedes llamar más TestBed métodos de configuración, no configureTestingModule() ni ninguno de los override... métodos. los TestBed arroja un error si lo intenta.

Hacer compileComponents() el último paso antes de llamar TestBed.createComponent().

El sincrónico antes de cada

El segundo, sincrónico beforeEach() contiene los pasos de configuración restantes, que incluyen la creación del componente y la consulta de elementos para inspeccionar.

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

Puede contar con el corredor de pruebas para esperar el primer asincrónico beforeEach para terminar antes de llamar al segundo.

Configuración consolidada

Puedes consolidar los dos beforeEach() funciones en una sola, asincrónica beforeEach().

los compileComponents() El método devuelve una promesa para que pueda realizar las tareas de configuración sincrónica. después compilación moviendo el código síncrono a un then(...) llamar de vuelta.

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

compileComponents () es inofensivo

No hay nada de malo en llamar compileComponents() cuando no es necesario.

El archivo de prueba de componentes generado por las llamadas CLI compileComponents() aunque nunca se requiere cuando se ejecuta ng test.

Las pruebas de esta guía solo requieren compileComponents cuando sea necesario.

Configuración con importaciones de módulos

Las pruebas de componentes anteriores configuraron el módulo de prueba con algunos declarations como esto:

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

los DashboardComponent es simple. No necesita ayuda. Pero los componentes más complejos a menudo dependen de otros componentes, directivas, conductos y proveedores, y estos también deben agregarse al módulo de prueba.

Afortunadamente, el TestBed.configureTestingModule parámetro es paralelo a los metadatos pasados ​​al @NgModule decorador, lo que significa que también puede especificar providers y imports.

los HeroDetailComponent requiere mucha ayuda a pesar de su pequeño tamaño y construcción simple. Además del soporte que recibe del módulo de prueba predeterminado CommonModule, necesita:

  • NgModel y amigos en el FormsModule para habilitar el enlace de datos bidireccional.
  • los TitleCasePipe desde el shared carpeta.
  • Servicios de enrutador (que estas pruebas son apéndice).
  • Servicios de acceso a datos Hero (también stubbed).

Un enfoque es configurar el módulo de prueba a partir de piezas individuales como en este ejemplo:

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

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

Note que el beforeEach() es asincrónico y llama TestBed.compileComponents porque el HeroDetailComponent tiene una plantilla externa y un archivo css.

Como se explica en Llamar a compileComponents () arriba, estas pruebas podrían ejecutarse en un entorno sin CLI donde Angular tendría que compilarlas en el navegador.

Importar un módulo compartido

Debido a que muchos componentes de la aplicación necesitan FormsModule y el TitleCasePipe, el desarrollador creó un SharedModule para combinar estas y otras piezas solicitadas con frecuencia.

La configuración de prueba puede utilizar el SharedModule también como se ve en esta configuración alternativa:

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

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

Es un poco más ajustado y más pequeño, con menos declaraciones de importación (no se muestran).

Importar un módulo de funciones

los HeroDetailComponent es parte del HeroModule Módulo de características que agrega más piezas interdependientes, incluido el SharedModule. Pruebe una configuración de prueba que importe el HeroModule como éste:

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

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

Esa es De Verdad crujiente. Solo el prueba de dobles en el providers permanecer. Incluso el HeroDetailComponent la declaración se ha ido.

De hecho, si intenta declararlo, Angular arrojará un error porque HeroDetailComponent se declara tanto en el HeroModule y el DynamicTestModule creado por el TestBed.

Importar el módulo de funciones del componente puede ser la forma más sencilla de configurar pruebas cuando hay muchas dependencias mutuas dentro del módulo y el módulo es pequeño, como suelen ser los módulos de funciones.

Proveedores de componentes de reemplazo

los HeroDetailComponent proporciona su propio HeroDetailService.

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

No es posible cortar el componente HeroDetailService en el providers de El TestBed.configureTestingModule. Esos son proveedores de módulo de prueba, no el componente. Preparan el inyector de dependencia en el nivel de accesorio.

Angular crea el componente con su propio inyector, que es un niño del inyector del accesorio. Registra los proveedores del componente (el HeroDetailService en este caso) con el inyector infantil.

Una prueba no puede llegar a los servicios de inyectores para niños desde el inyector de accesorios. Y TestBed.configureTestingModule tampoco puede configurarlos.

Angular ha estado creando nuevas instancias de lo real. HeroDetailService ¡todo el tiempo!

Estas pruebas pueden fallar o agotarse si el HeroDetailService hizo sus propias llamadas XHR a un servidor remoto. Puede que no haya un servidor remoto al que llamar.

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

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

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

¿Y si no tienes tanta suerte? ¿Y si fingiendo el HeroService ¿es difícil? Y si HeroDetailService hace sus propias solicitudes de servidor?

los TestBed.overrideComponent El método puede reemplazar el componente providers con fácil de administrar prueba de dobles como se ve en la siguiente variación de configuración:

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

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

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

      .compileComponents();
});

Darse cuenta de TestBed.configureTestingModule ya no proporciona un (falso) HeroService porque no es necesario.

los overrideComponent método

Centrarse en el overrideComponent método.

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

Se necesitan dos argumentos: el tipo de componente a anular (HeroDetailComponent) y un objeto de anulación de metadatos. El objeto de anulación de metadatos es un genérico definido de la siguiente manera:

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

Un objeto de anulación de metadatos puede agregar y eliminar elementos en las propiedades de los metadatos o restablecer por completo esas propiedades. Este ejemplo restablece el componente providers metadatos.

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

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

Proporcionar una talón de espía (HeroDetailServiceSpy)

Este ejemplo reemplaza completamente el componente providers matriz con una nueva matriz que contiene un HeroDetailServiceSpy.

los HeroDetailServiceSpy es una versión cortada del real HeroDetailService que falsifica todas las características necesarias de ese servicio. No inyecta ni delega al nivel inferior HeroService por lo que no es necesario proporcionar un doble de prueba para eso.

El relacionado HeroDetailComponent Las pruebas afirmarán que los métodos de la HeroDetailService fueron llamados espiando los métodos de servicio. En consecuencia, el stub implementa sus métodos como espías:

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

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

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

Las pruebas de anulación

Ahora las pruebas pueden controlar al héroe del componente directamente manipulando el código de espionaje. testHero y confirme que se llamaron a los métodos de servicio.

let hdsSpy: HeroDetailServiceSpy;

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

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

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

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

     page.nameInput.value = newName;

     // In older browsers, such as IE, you might need a CustomEvent instead. See
     // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
     page.nameInput.dispatchEvent(new Event('input')); // tell Angular

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

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

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

Más anulaciones

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

Explore las opciones y combinaciones por su cuenta.