Esta página describe las características de pruebas de Angular más útiles.
Las utilidades de pruebas de Angular incluyen el TestBed, el ComponentFixture y un puñado de funciones que controlan el entorno de prueba.
Las clases TestBed y ComponentFixture se cubren por separado.
Aquí hay un resumen de las funciones independientes, en orden de utilidad probable:
| Función | Detalles |
|---|---|
waitForAsync |
Ejecuta el cuerpo de una prueba (it) o función de configuración (beforeEach) dentro de una zona de prueba async especial. Ver waitForAsync. |
fakeAsync |
Ejecuta el cuerpo de una prueba (it) dentro de una zona de prueba fakeAsync especial, habilitando un estilo de codificación de flujo de control lineal. Ver fakeAsync. |
tick |
Simula el paso del tiempo y la finalización de actividades asíncronas pendientes vaciando tanto las colas de timer como de micro-task dentro de la zona de prueba fakeAsync. El lector curioso y dedicado podría disfrutar esta larga publicación de blog, "Tasks, microtasks, queues and schedules". Acepta un argumento opcional que mueve el reloj virtual hacia adelante por el número especificado de milisegundos, limpiando actividades asíncronas programadas dentro de ese marco de tiempo. Ver tick. |
inject |
Inyecta uno o más servicios del inyector TestBed actual en una función de prueba. No puede inyectar un servicio proporcionado por el componente mismo. Ver discusión de debugElement.injector. |
discardPeriodicTasks |
Cuando una prueba fakeAsync() termina con tasks de evento de timer pendientes (callbacks setTimeOut y setInterval en cola), la prueba falla con un mensaje de error claro. En general, una prueba debería terminar sin tareas en cola. Cuando se esperan tareas de timer pendientes, llama a discardPeriodicTasks para vaciar la cola de task y evitar el error. |
flushMicrotasks |
Cuando una prueba fakeAsync() termina con micro-tasks pendientes como promesas no resueltas, la prueba falla con un mensaje de error claro. En general, una prueba debería esperar a que las micro-tasks finalicen. Cuando se esperan microtasks pendientes, llama a flushMicrotasks para vaciar la cola de micro-task y evitar el error. |
ComponentFixtureAutoDetect |
Un token de provider para un servicio que activa la detección automática de cambios. |
getTestBed |
Obtiene la instancia actual del TestBed. Usualmente innecesario porque los métodos de clase estáticos de la clase TestBed son típicamente suficientes. La instancia TestBed expone algunos miembros raramente usados que no están disponibles como métodos estáticos. |
Resumen de la clase TestBed
La clase TestBed es una de las utilidades principales de pruebas de Angular.
Su API es bastante grande y puede ser abrumadora hasta que la hayas explorado, un poco a la vez.
Lee la parte temprana de esta guía primero para obtener lo básico antes de intentar absorber la API completa.
La definición de módulo pasada a configureTestingModule es un subconjunto de las propiedades de metadata @NgModule.
type TestModuleMetadata = { providers?: any[]; declarations?: any[]; imports?: any[]; schemas?: Array<SchemaMetadata | any[]>;};
Cada método override toma un MetadataOverride<T> donde T es el tipo de metadata apropiado para el método, es decir, el parámetro de un @NgModule, @Component, @Directive o @Pipe.
type MetadataOverride<T> = { add?: Partial<T>; remove?: Partial<T>; set?: Partial<T>;};
La API del TestBed consiste en métodos de clase estáticos que actualizan o referencian una instancia global del TestBed.
Internamente, todos los métodos estáticos cubren métodos de la instancia TestBed de runtime actual, que también es retornada por la función getTestBed().
Llama a los métodos TestBed dentro de un beforeEach() para asegurar un comienzo fresco antes de cada prueba individual.
Aquí están los métodos estáticos más importantes, en orden de utilidad probable.
| Métodos | Detalles |
|---|---|
configureTestingModule |
Los shims de testing (karma-test-shim, browser-test-shim) establecen el entorno de prueba inicial y un módulo de testing predeterminado. El módulo de testing predeterminado está configurado con declarativas básicas y algunos sustitutos de servicio de Angular que cada probador necesita. Llama a configureTestingModule para refinar la configuración del módulo de testing para un conjunto particular de pruebas agregando y eliminando imports, declarations (de componentes, directivas y pipes), y providers. |
compileComponents |
Compila el módulo de testing de forma asíncrona después de que hayas terminado de configurarlo. Debes llamar a este método si alguno de los componentes del módulo de testing tiene un templateUrl o styleUrls porque obtener archivos de plantilla y estilo del componente es necesariamente asíncrono. Ver compileComponents. Después de llamar a compileComponents, la configuración del TestBed se congela durante la duración del spec actual. |
createComponent<T> |
Crea una instancia de un componente de tipo T basado en la configuración actual del TestBed. Después de llamar a createComponent, la configuración del TestBed se congela durante la duración del spec actual. |
overrideModule |
Reemplaza metadata para el NgModule dado. Recuerda que los módulos pueden importar otros módulos. El método overrideModule puede llegar profundamente al módulo de testing actual para modificar uno de estos módulos internos. |
overrideComponent |
Reemplaza metadata para la clase de componente dada, que podría estar anidada profundamente dentro de un módulo interno. |
overrideDirective |
Reemplaza metadata para la clase de directiva dada, que podría estar anidada profundamente dentro de un módulo interno. |
overridePipe |
Reemplaza metadata para la clase de pipe dada, que podría estar anidada profundamente dentro de un módulo interno. |
inject |
Recupera un servicio del inyector TestBed actual. La función inject es a menudo adecuada para este propósito. Pero inject lanza un error si no puede proporcionar el servicio. ¿Qué pasa si el servicio es opcional? El método TestBed.inject() toma un segundo parámetro opcional, el objeto a retornar si Angular no puede encontrar el provider (null en este ejemplo): TestBed.inject, la configuración del TestBed se congela durante la duración del spec actual. |
initTestEnvironment |
Inicializa el entorno de testing para toda la ejecución de pruebas. Los shims de testing ( karma-test-shim, browser-test-shim) lo llaman por ti así que raramente hay una razón para que lo llames tú mismo. Llama a este método exactamente una vez. Para cambiar este predeterminado en medio de una ejecución de pruebas, llama primero a resetTestEnvironment. Especifica la factory del compilador de Angular, un PlatformRef, y un módulo de testing de Angular predeterminado. Alternativas para plataformas no-navegador están disponibles en la forma general @angular/platform-<platform_name>/testing/<platform_name>. |
resetTestEnvironment |
Reinicia el entorno de prueba inicial, incluyendo el módulo de testing predeterminado. |
Algunos de los métodos de instancia del TestBed no están cubiertos por métodos de clase TestBed estáticos.
Estos rara vez se necesitan.
El ComponentFixture
El TestBed.createComponent<T> crea una instancia del componente T y retorna un ComponentFixture fuertemente tipado para ese componente.
Las propiedades y métodos del ComponentFixture proporcionan acceso al componente, su representación DOM y aspectos de su entorno Angular.
Propiedades de ComponentFixture
Aquí están las propiedades más importantes para los probadores, en orden de utilidad probable.
| Propiedades | Detalles |
|---|---|
componentInstance |
La instancia de la clase del componente creada por TestBed.createComponent. |
debugElement |
El DebugElement asociado con el elemento raíz del componente. El debugElement proporciona información sobre el componente y su elemento DOM durante la prueba y depuración. Es una propiedad crítica para los probadores. Los miembros más interesantes se cubren abajo. |
nativeElement |
El elemento DOM nativo en la raíz del componente. |
changeDetectorRef |
El ChangeDetectorRef para el componente. El ChangeDetectorRef es más valioso cuando se prueba un componente que tiene el método ChangeDetectionStrategy.OnPush o la detección de cambios del componente está bajo tu control programático. |
Métodos de ComponentFixture
Los métodos fixture hacen que Angular realice ciertas tareas en el árbol de componentes. Llama a estos métodos para desencadenar comportamiento de Angular en respuesta a una acción de usuario simulada.
Aquí están los métodos más útiles para los probadores.
| Métodos | Detalles |
|---|---|
detectChanges |
Desencadena un ciclo de detección de cambios para el componente. Llámalo para inicializar el componente (llama a ngOnInit) y después de que tu código de prueba cambie los valores de propiedad vinculados a datos del componente. Angular no puede ver que has cambiado personComponent.name y no actualizará el binding name hasta que llames a detectChanges. Ejecuta checkNoChanges después para confirmar que no hay actualizaciones circulares a menos que se llame como detectChanges(false); |
autoDetectChanges |
Establece esto a true cuando quieras que el fixture detecte cambios automáticamente. Cuando autodetect es true, el test fixture llama a detectChanges inmediatamente después de crear el componente. Luego escucha eventos de zona pertinentes y llama a detectChanges en consecuencia. Cuando tu código de prueba modifica valores de propiedad del componente directamente, probablemente aún tengas que llamar a fixture.detectChanges para desencadenar actualizaciones de binding de datos. El predeterminado es false. Los probadores que prefieren control fino sobre el comportamiento de prueba tienden a mantenerlo en false. |
checkNoChanges |
Ejecuta una ejecución de detección de cambios para asegurarse de que no hay cambios pendientes. Lanza excepciones si los hay. |
isStable |
Si el fixture es actualmente estable, retorna true. Si hay tareas async que no se han completado, retorna false. |
whenStable |
Retorna una promesa que se resuelve cuando el fixture es estable. Para reanudar las pruebas después de la finalización de actividad asíncrona o detección de cambios asíncrona, engancha esa promesa. Ver whenStable. |
destroy |
Desencadena la destrucción del componente. |
DebugElement
El DebugElement proporciona información crucial sobre la representación DOM del componente.
Desde el DebugElement del componente raíz de prueba retornado por fixture.debugElement, puedes caminar (y consultar) todo el subárbol de elementos y componentes del fixture.
Aquí están los miembros más útiles de DebugElement para los probadores, en orden aproximado de utilidad:
| Miembros | Detalles |
|---|---|
nativeElement |
El elemento DOM correspondiente en el navegador |
query |
Llamar a query(predicate: Predicate<DebugElement>) retorna el primer DebugElement que coincide con el predicate a cualquier profundidad en el subárbol. |
queryAll |
Llamar a queryAll(predicate: Predicate<DebugElement>) retorna todos los DebugElements que coinciden con el predicate a cualquier profundidad en el subárbol. |
injector |
El inyector de dependencias del host. Por ejemplo, el inyector de instancia del componente del elemento raíz. |
componentInstance |
La propia instancia del componente del elemento, si tiene una. |
context |
Un objeto que proporciona contexto padre para este elemento. A menudo una instancia de componente ancestro que gobierna este elemento. Cuando un elemento se repite dentro de un bloque @for, el contexto es un RepeaterContext cuya propiedad $implicit es el valor de la instancia de la fila. Por ejemplo, el hero en @for(hero of heroes; ...). |
children |
Los hijos DebugElement inmediatos. Camina el árbol descendiendo a través de children. DebugElement también tiene childNodes, una lista de objetos DebugNode. DebugElement deriva de objetos DebugNode y a menudo hay más nodos que elementos. Los probadores usualmente pueden ignorar nodos planos. |
parent |
El padre DebugElement. Null si este es el elemento raíz. |
name |
El nombre de tag del elemento, si es un elemento. |
triggerEventHandler |
Desencadena el evento por su nombre si hay un listener correspondiente en la colección listeners del elemento. El segundo parámetro es el objeto evento esperado por el handler. Ver triggerEventHandler. Si el evento carece de un listener o hay algún otro problema, considera llamar a nativeElement.dispatchEvent(eventObject). |
listeners |
Los callbacks adjuntos a las propiedades @Output del componente y/o las propiedades de evento del elemento. |
providerTokens |
Los tokens de lookup del inyector de este componente. Incluye el componente mismo más los tokens que el componente lista en su metadata providers. |
source |
Dónde encontrar este elemento en la plantilla del componente fuente. |
references |
Diccionario de objetos asociados con variables locales de plantilla (por ejemplo, #foo), con clave del nombre de la variable local. |
Los métodos DebugElement.query(predicate) y DebugElement.queryAll(predicate) toman un predicate que filtra el subárbol del elemento fuente para coincidencias DebugElement.
El predicate es cualquier método que toma un DebugElement y retorna un valor truthy.
El siguiente ejemplo encuentra todos los DebugElements con una referencia a una variable local de plantilla llamada "content":
app/demo/demo.testbed.spec.ts
import {Component, DebugElement, Injectable} from '@angular/core';import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync,} from '@angular/core/testing';import {FormsModule, NgControl, NgModel} from '@angular/forms';import {By} from '@angular/platform-browser';import {addMatchers, click} from '../../testing';import { BankAccountComponent, BankAccountParentComponent, Child1Component, Child2Component, Child3Component, ExternalTemplateComponent, InputComponent, IoComponent, IoParentComponent, LightswitchComponent, MasterService, MyIfChildComponent, MyIfComponent, MyIfParentComponent, NeedsContentComponent, ParentComponent, ReversePipeComponent, ShellComponent, TestProvidersComponent, TestViewProvidersComponent, ValueService,} from './demo';export class NotProvided extends ValueService { /* example below */}beforeEach(addMatchers);describe('demo (with TestBed):', () => { //////// Service Tests ///////////// describe('ValueService', () => { let service: ValueService; beforeEach(() => { TestBed.configureTestingModule({providers: [ValueService]}); service = TestBed.inject(ValueService); }); it('should use ValueService', () => { service = TestBed.inject(ValueService); expect(service.getValue()).toBe('real value'); }); it('can inject a default value when service is not provided', () => { expect(TestBed.inject(NotProvided, null)).toBeNull(); }); it('test should wait for ValueService.getPromiseValue', waitForAsync(() => { service.getPromiseValue().then((value) => expect(value).toBe('promise value')); })); it('test should wait for ValueService.getObservableValue', waitForAsync(() => { service.getObservableValue().subscribe((value) => expect(value).toBe('observable value')); })); // Must use done. See https://github.com/angular/angular/issues/10127 it('test should wait for ValueService.getObservableDelayValue', (done: DoneFn) => { service.getObservableDelayValue().subscribe((value) => { expect(value).toBe('observable delay value'); done(); }); }); it('should allow the use of fakeAsync', fakeAsync(() => { let value: any; service.getPromiseValue().then((val: any) => (value = val)); tick(); // Trigger JS engine cycle until all promises resolve. expect(value).toBe('promise value'); })); }); describe('MasterService', () => { let masterService: MasterService; let valueServiceSpy: jasmine.SpyObj<ValueService>; beforeEach(() => { const spy = jasmine.createSpyObj('ValueService', ['getValue']); TestBed.configureTestingModule({ // Provide both the service-to-test and its (spy) dependency providers: [MasterService, {provide: ValueService, useValue: spy}], }); // Inject both the service-to-test and its (spy) dependency masterService = TestBed.inject(MasterService); valueServiceSpy = TestBed.inject(ValueService) as jasmine.SpyObj<ValueService>; }); it('#getValue should return stubbed value from a spy', () => { const stubValue = 'stub value'; valueServiceSpy.getValue.and.returnValue(stubValue); expect(masterService.getValue()).withContext('service returned stub value').toBe(stubValue); expect(valueServiceSpy.getValue.calls.count()) .withContext('spy method was called once') .toBe(1); expect(valueServiceSpy.getValue.calls.mostRecent().returnValue).toBe(stubValue); }); }); describe('use inject within `it`', () => { beforeEach(() => { TestBed.configureTestingModule({providers: [ValueService]}); }); it('should use modified providers', inject([ValueService], (service: ValueService) => { service.setValue('value modified in beforeEach'); expect(service.getValue()).toBe('value modified in beforeEach'); })); }); describe('using waitForAsync(inject) within beforeEach', () => { let serviceValue: string; beforeEach(() => { TestBed.configureTestingModule({providers: [ValueService]}); }); beforeEach(waitForAsync( inject([ValueService], (service: ValueService) => { service.getPromiseValue().then((value) => (serviceValue = value)); }), )); it('should use asynchronously modified value ... in synchronous test', () => { expect(serviceValue).toBe('promise value'); }); }); /////////// Component Tests ////////////////// describe('TestBed component tests', () => { // beforeEach(waitForAsync(() => { // TestBed.configureTestingModule() // // Compile everything in DemoModule // ; // })); it('should create a component with inline template', () => { const fixture = TestBed.createComponent(Child1Component); fixture.detectChanges(); expect(fixture).toHaveText('Child'); }); it('should create a component with external template', () => { const fixture = TestBed.createComponent(ExternalTemplateComponent); fixture.detectChanges(); expect(fixture).toHaveText('from external template'); }); it('should allow changing members of the component', () => { const fixture = TestBed.createComponent(MyIfComponent); fixture.detectChanges(); expect(fixture).toHaveText('MyIf()'); fixture.componentInstance.showMore = true; fixture.detectChanges(); expect(fixture).toHaveText('MyIf(More)'); }); it('should create a nested component bound to inputs/outputs', () => { const fixture = TestBed.createComponent(IoParentComponent); fixture.detectChanges(); const heroes = fixture.debugElement.queryAll(By.css('.hero')); expect(heroes.length).withContext('has heroes').toBeGreaterThan(0); const comp = fixture.componentInstance; const hero = comp.heroes[0]; click(heroes[0]); fixture.detectChanges(); const selected = fixture.debugElement.query(By.css('p')); expect(selected).toHaveText(hero.name); }); it('can access the instance variable of an `*ngFor` row component', () => { const fixture = TestBed.createComponent(IoParentComponent); const comp = fixture.componentInstance; const heroName = comp.heroes[0].name; // first hero's name fixture.detectChanges(); const ngForRow = fixture.debugElement.query(By.directive(IoComponent)); // first hero ngForRow const hero = ngForRow.context.hero; // the hero object passed into the row expect(hero.name).withContext('ngRow.context.hero').toBe(heroName); const rowComp = ngForRow.componentInstance; // jasmine.any is an "instance-of-type" test. expect(rowComp).withContext('component is IoComp').toEqual(jasmine.any(IoComponent)); expect(rowComp.hero.name).withContext('component.hero').toBe(heroName); }); it('should support clicking a button', () => { const fixture = TestBed.createComponent(LightswitchComponent); const btn = fixture.debugElement.query(By.css('button')); const span = fixture.debugElement.query(By.css('span')).nativeElement; fixture.detectChanges(); expect(span.textContent) .withContext('before click') .toMatch(/is off/i); click(btn); fixture.detectChanges(); expect(span.textContent).withContext('after click').toMatch(/is on/i); }); // ngModel is async so we must wait for it with promise-based `whenStable` it('should support entering text in input box (ngModel)', waitForAsync(() => { const expectedOrigName = 'John'; const expectedNewName = 'Sally'; const fixture = TestBed.createComponent(InputComponent); fixture.detectChanges(); const comp = fixture.componentInstance; const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; expect(comp.name) .withContext(`At start name should be ${expectedOrigName} `) .toBe(expectedOrigName); // wait until ngModel binds comp.name to input box fixture .whenStable() .then(() => { expect(input.value) .withContext( `After ngModel updates input box, input.value should be ${expectedOrigName} `, ) .toBe(expectedOrigName); // simulate user entering new name in input input.value = expectedNewName; // that change doesn't flow to the component immediately expect(comp.name()) .withContext( `comp.name should still be ${expectedOrigName} after value change, before binding happens`, ) .toBe(expectedOrigName); // Dispatch a DOM event so that Angular learns of input value change. // then wait while ngModel pushes input.box value to comp.name input.dispatchEvent(new Event('input')); return fixture.whenStable(); }) .then(() => { expect(comp.name()) .withContext(`After ngModel updates the model, comp.name should be ${expectedNewName} `) .toBe(expectedNewName); }); })); // fakeAsync version of ngModel input test enables sync test style // synchronous `tick` replaces asynchronous promise-base `whenStable` it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => { const expectedOrigName = 'John'; const expectedNewName = 'Sally'; const fixture = TestBed.createComponent(InputComponent); fixture.detectChanges(); const comp = fixture.componentInstance; const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; expect(comp.name) .withContext(`At start name should be ${expectedOrigName} `) .toBe(expectedOrigName); // wait until ngModel binds comp.name to input box tick(); expect(input.value) .withContext(`After ngModel updates input box, input.value should be ${expectedOrigName} `) .toBe(expectedOrigName); // simulate user entering new name in input input.value = expectedNewName; // that change doesn't flow to the component immediately expect(comp.name) .withContext( `comp.name should still be ${expectedOrigName} after value change, before binding happens`, ) .toBe(expectedOrigName); // Dispatch a DOM event so that Angular learns of input value change. // then wait a tick while ngModel pushes input.box value to comp.name input.dispatchEvent(new Event('input')); tick(); expect(comp.name) .withContext(`After ngModel updates the model, comp.name should be ${expectedNewName} `) .toBe(expectedNewName); })); it('ReversePipeComp should reverse the input text', fakeAsync(() => { const inputText = 'the quick brown fox.'; const expectedText = '.xof nworb kciuq eht'; const fixture = TestBed.createComponent(ReversePipeComponent); fixture.detectChanges(); const comp = fixture.componentInstance; const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement; const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement; // simulate user entering new name in input input.value = inputText; // Dispatch a DOM event so that Angular learns of input value change. // then wait a tick while ngModel pushes input.box value to comp.text // and Angular updates the output span input.dispatchEvent(new Event('input')); tick(); fixture.detectChanges(); expect(span.textContent).withContext('output span').toBe(expectedText); expect(comp.text).withContext('component.text').toBe(inputText); })); // Use this technique to find attached directives of any kind it('can examine attached directives and listeners', () => { const fixture = TestBed.createComponent(InputComponent); fixture.detectChanges(); const inputEl = fixture.debugElement.query(By.css('input')); expect(inputEl.providerTokens).withContext('NgModel directive').toContain(NgModel); const ngControl = inputEl.injector.get(NgControl); expect(ngControl).withContext('NgControl directive').toEqual(jasmine.any(NgControl)); expect(inputEl.listeners.length).withContext('several listeners attached').toBeGreaterThan(2); }); it('BankAccountComponent should set attributes, styles, classes, and properties', () => { const fixture = TestBed.createComponent(BankAccountParentComponent); fixture.detectChanges(); const comp = fixture.componentInstance; // the only child is debugElement of the BankAccount component const el = fixture.debugElement.children[0]; const childComp = el.componentInstance as BankAccountComponent; expect(childComp).toEqual(jasmine.any(BankAccountComponent)); expect(el.context).withContext('context is the child component').toBe(childComp); expect(el.attributes['account']).withContext('account attribute').toBe(childComp.id); expect(el.attributes['bank']).withContext('bank attribute').toBe(childComp.bank); expect(el.classes['closed']).withContext('closed class').toBe(true); expect(el.classes['open']).withContext('open class').toBeFalsy(); expect(el.styles['color']).withContext('color style').toBe(comp.color); expect(el.styles['width']) .withContext('width style') .toBe(comp.width + 'px'); // Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future? // expect(el.properties['customProperty']).toBe(true, 'customProperty'); }); }); describe('TestBed component overrides:', () => { it("should override ChildComp's template", () => { const fixture = TestBed.configureTestingModule({ imports: [Child1Component], }) .overrideComponent(Child1Component, { set: {template: '<span>Fake</span>'}, }) .createComponent(Child1Component); fixture.detectChanges(); expect(fixture).toHaveText('Fake'); }); it("should override TestProvidersComp's ValueService provider", () => { const fixture = TestBed.configureTestingModule({ imports: [TestProvidersComponent], }) .overrideComponent(TestProvidersComponent, { remove: {providers: [ValueService]}, add: {providers: [{provide: ValueService, useClass: FakeValueService}]}, // Or replace them all (this component has only one provider) // set: { providers: [{ provide: ValueService, useClass: FakeValueService }] }, }) .createComponent(TestProvidersComponent); fixture.detectChanges(); expect(fixture).toHaveText('injected value: faked value', 'text'); // Explore the providerTokens const tokens = fixture.debugElement.providerTokens; expect(tokens).withContext('component ctor').toContain(fixture.componentInstance.constructor); expect(tokens).withContext('TestProvidersComp').toContain(TestProvidersComponent); expect(tokens).withContext('ValueService').toContain(ValueService); }); it("should override TestViewProvidersComp's ValueService viewProvider", () => { const fixture = TestBed.configureTestingModule({ imports: [TestViewProvidersComponent], }) .overrideComponent(TestViewProvidersComponent, { // remove: { viewProviders: [ValueService]}, // add: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] // }, // Or replace them all (this component has only one viewProvider) set: {viewProviders: [{provide: ValueService, useClass: FakeValueService}]}, }) .createComponent(TestViewProvidersComponent); fixture.detectChanges(); expect(fixture).toHaveText('injected value: faked value'); }); it("injected provider should not be same as component's provider", () => { // TestComponent is parent of TestProvidersComponent @Component({ template: '<my-service-comp></my-service-comp>', imports: [TestProvidersComponent], }) class TestComponent {} // 3 levels of ValueService provider: module, TestComponent, TestProvidersComponent const fixture = TestBed.configureTestingModule({ imports: [TestComponent, TestProvidersComponent], providers: [ValueService], }) .overrideComponent(TestComponent, { set: {providers: [{provide: ValueService, useValue: {}}]}, }) .overrideComponent(TestProvidersComponent, { set: {providers: [{provide: ValueService, useClass: FakeValueService}]}, }) .createComponent(TestComponent); let testBedProvider!: ValueService; // `inject` uses TestBed's injector inject([ValueService], (s: ValueService) => (testBedProvider = s))(); const tcProvider = fixture.debugElement.injector.get(ValueService) as ValueService; const tpcProvider = fixture.debugElement.children[0].injector.get( ValueService, ) as FakeValueService; expect(testBedProvider).withContext('testBed/tc not same providers').not.toBe(tcProvider); expect(testBedProvider).withContext('testBed/tpc not same providers').not.toBe(tpcProvider); expect(testBedProvider instanceof ValueService) .withContext('testBedProvider is ValueService') .toBe(true); expect(tcProvider) .withContext('tcProvider is {}') .toEqual({} as ValueService); expect(tpcProvider instanceof FakeValueService) .withContext('tpcProvider is FakeValueService') .toBe(true); }); it('can access template local variables as references', () => { const fixture = TestBed.configureTestingModule({ imports: [ ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component, ], }) .overrideComponent(ShellComponent, { set: { selector: 'test-shell', imports: [NeedsContentComponent, Child1Component, Child2Component, Child3Component], template: ` <needs-content #nc> <child-1 #content text="My"></child-1> <child-2 #content text="dog"></child-2> <child-2 text="has"></child-2> <child-3 #content text="fleas"></child-3> <div #content>!</div> </needs-content> `, }, }) .createComponent(ShellComponent); fixture.detectChanges(); // NeedsContentComp is the child of ShellComp const el = fixture.debugElement.children[0]; const comp = el.componentInstance; expect(comp.children.toArray().length) .withContext('three different child components and an ElementRef with #content') .toBe(4); expect(el.references['nc']).withContext('#nc reference to component').toBe(comp); // Filter for DebugElements with a #content reference const contentRefs = el.queryAll((de) => de.references['content']); expect(contentRefs.length).withContext('elements w/ a #content reference').toBe(4); }); }); describe('nested (one-deep) component override', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ParentComponent, FakeChildComponent], }).overrideComponent(ParentComponent, { set: {imports: [FakeChildComponent]}, }); }); it('ParentComp should use Fake Child component', () => { const fixture = TestBed.createComponent(ParentComponent); fixture.detectChanges(); expect(fixture).toHaveText('Parent(Fake Child)'); }); }); describe('nested (two-deep) component override', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent], }).overrideComponent(ParentComponent, { set: {imports: [FakeChildWithGrandchildComponent, FakeGrandchildComponent]}, }); }); it('should use Fake Grandchild component', () => { const fixture = TestBed.createComponent(ParentComponent); fixture.detectChanges(); expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))'); }); });});////////// Fakes ///////////@Component({ selector: 'child-1', template: 'Fake Child',})class FakeChildComponent {}@Component({ selector: 'grandchild-1', template: 'Fake Grandchild',})class FakeGrandchildComponent {}@Component({ selector: 'child-1', imports: [FakeGrandchildComponent], template: 'Fake Child(<grandchild-1></grandchild-1>)',})class FakeChildWithGrandchildComponent {}@Injectable()class FakeValueService extends ValueService { override value = 'faked value';}
La clase By de Angular tiene tres métodos estáticos para predicates comunes:
| Método estático | Detalles |
|---|---|
By.all |
Retorna todos los elementos |
By.css(selector) |
Retorna elementos con selectores CSS coincidentes |
By.directive(directive) |
Retorna elementos que Angular coincidió con una instancia de la clase de directiva |
app/hero/hero-list.component.spec.ts
import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing';import {By} from '@angular/platform-browser';import {DebugElement} from '@angular/core';import {Router} from '@angular/router';import {addMatchers} from '../../testing';import {HeroService} from '../model/hero.service';import {getTestHeroes, TestHeroService} from '../model/testing/test-hero.service';import {HeroListComponent} from './hero-list.component';import {HighlightDirective} from '../shared/highlight.directive';import {appConfig} from '../app.config';const HEROES = getTestHeroes();let comp: HeroListComponent;let fixture: ComponentFixture<HeroListComponent>;let page: Page;/////// Tests //////describe('HeroListComponent', () => { beforeEach(waitForAsync(() => { addMatchers(); const routerSpy = jasmine.createSpyObj('Router', ['navigate']); TestBed.configureTestingModule( Object.assign({}, appConfig, { providers: [ {provide: HeroService, useClass: TestHeroService}, {provide: Router, useValue: routerSpy}, ], }), ) .then(createComponent); })); it('should display heroes', () => { expect(page.heroRows.length).toBeGreaterThan(0); }); it('1st hero should match 1st test hero', () => { const expectedHero = HEROES[0]; const actualHero = page.heroRows[0].textContent; expect(actualHero).withContext('hero.id').toContain(expectedHero.id.toString()); expect(actualHero).withContext('hero.name').toContain(expectedHero.name); }); it('should select hero on click', fakeAsync(() => { const expectedHero = HEROES[1]; const btn = page.heroRows[1].querySelector('button'); btn!.dispatchEvent(new Event('click')); tick(); // `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService expect(comp.selectedHero).toEqual(expectedHero); })); it('should navigate to selected hero detail on click', fakeAsync(() => { const expectedHero = HEROES[1]; const btn = page.heroRows[1].querySelector('button'); btn!.dispatchEvent(new Event('click')); tick(); // should have navigated expect(page.navSpy.calls.any()).withContext('navigate called').toBe(true); // composed hero detail will be URL like 'heroes/42' // expect link array with the route path and hero id // first argument to router.navigate is link array const navArgs = page.navSpy.calls.first().args[0]; expect(navArgs[0]).withContext('nav to heroes detail URL').toContain('heroes'); expect(navArgs[1]).withContext('expected hero.id').toBe(expectedHero.id); })); it('should find `HighlightDirective` with `By.directive', () => { // Can find DebugElement either by css selector or by directive const h2 = fixture.debugElement.query(By.css('h2')); const directive = fixture.debugElement.query(By.directive(HighlightDirective)); expect(h2).toBe(directive); }); it('should color header with `HighlightDirective`', () => { const h2 = page.highlightDe.nativeElement as HTMLElement; const bgColor = h2.style.backgroundColor; // different browsers report color values differently const isExpectedColor = bgColor === 'gold' || bgColor === 'rgb(255, 215, 0)'; expect(isExpectedColor).withContext('backgroundColor').toBe(true); }); it("the `HighlightDirective` is among the element's providers", () => { expect(page.highlightDe.providerTokens) .withContext('HighlightDirective') .toContain(HighlightDirective); });});/////////// Helpers //////** Create the component and set the `page` test variables */function createComponent() { fixture = TestBed.createComponent(HeroListComponent); comp = fixture.componentInstance; // change detection triggers ngOnInit which gets a hero fixture.detectChanges(); return fixture.whenStable().then(() => { // got the heroes and updated component // change detection updates the view fixture.detectChanges(); page = new Page(); });}class Page { /** Hero line elements */ heroRows: HTMLLIElement[]; /** Highlighted DebugElement */ highlightDe: DebugElement; /** Spy on router navigate method */ navSpy: jasmine.Spy; constructor() { const heroRowNodes = fixture.nativeElement.querySelectorAll('li'); this.heroRows = Array.from(heroRowNodes); // Find the first element with an attached HighlightDirective this.highlightDe = fixture.debugElement.query(By.directive(HighlightDirective)); // Get the component's injected router navigation spy const routerSpy = fixture.debugElement.injector.get(Router); this.navSpy = routerSpy.navigate as jasmine.Spy; }}