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 llamardetectChanges()
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 unXMLHttpRequest
(XHR) llamada. Las llamadas XHR dentro de una prueba son raras, pero si necesita llamar a XHR, consultewaitForAsync()
, 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 unbutton
propiedad que identifica qué botón del mouse (si lo hay) se presionó durante el clic. losRouterLink
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:
- Eso declara ambos
DashboardHeroComponent
y elTestHostComponent
. - Eso crea los
TestHostComponent
en vez deDashboardHeroComponent
. - los
TestHostComponent
establece elDashboardHeroComponent.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 unObservable
que puede emitir más de un valor durante una prueba. - Necesita la función de ayuda del enrutador,
convertToParamMap()
, para crear unParamMap
. - 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 ypage
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:
-
Puede localizar los elementos de anclaje con una directiva adjunta utilizando
By.directive
. -
La consulta devuelve
DebugElement
envoltorios alrededor de los elementos coincidentes. -
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.
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 errorError: ViewDestroyedError: Attempt to use a destroyed view
Un enfoque típico es dividir la lógica de configuración en dos beforeEach()
funciones:
- Un async
beforeEach()
que compila los componentes - 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 llamarcompileComponents()
.
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 elFormsModule
para habilitar el enlace de datos bidireccional. - los
TitleCasePipe
desde elshared
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 llamaTestBed.compileComponents
porque elHeroDetailComponent
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 inyectadoHeroService
.@Injectable() export class HeroDetailService { constructor(private heroService: HeroService) { } /* . . . */ }La configuración de prueba anterior reemplaza a la real
HeroService
con unTestHeroService
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.