¿Cómo probar el layout en Angular asegurando cobertura completa?
Al trabajar en pruebas para garantizar un 100% de cobertura en nuestras funciones frontend, es crítico entender cómo implementar tests efectivos en nuestros componentes de Angular, especialmente en el layout. Aquí te guiamos sobre cómo realizar estas pruebas avanzadas.
¿Cómo configuramos el testbed y los servicios?
Como primer paso, configuramos el testbed, esencial en cualquier prueba en Angular. Nuestro componente layout utiliza dos servicios que debemos gestionar: el bottom sheet y el router.
Creación de Stubs para el Bottom Sheet:
Define un stub para el servicio BottomSheet con un método open.
classBottomSheetStub{open(){}}
Configuración del Router:
Utiliza RouterTestingModule para manejar rutas en pruebas.
Define rutas, incluyendo la raíz y una para habilitar el modo de edición.
Para evitar errores ligados a componentes no definidos:
Asegúrate de importar esquemas relacionados con Angular Core, como CUSTOM_ELEMENTS_SCHEMA. Esto previene problemas al usar elementos personalizados:
schemas:[CUSTOM_ELEMENTS_SCHEMA]
¿Cómo implementar y verificar las pruebas?
Con nuestro testbed configurado, avanzamos con las pruebas.
Verificar editMode en falso:
Navega a la raíz y asegúrate de que editMode es false.
Usa el método whenStable para confirmar que el cambio en la aplicación quedó registrado.
it('should set editMode to false',async()=>{// Configura el espía para verifyEditModespyOn(component,'verifyEditMode');await fixture.whenStable();expect(component.editMode).toBe(false);expect(component.verifyEditMode).toHaveBeenCalled();});
Prueba sin errores ng-zone:
Usa NgZone para garantizar que se reflejen los cambios correctamente.
it('should open button sheet with ActionsComponent',()=>{const openSpy =spyOn(component['bottomSheet'],'open'); component.openButtonSheet();expect(openSpy).toHaveBeenCalled();expect(openSpy).toHaveBeenCalledWith(ActionsComponent);});
¿Qué retos adicionales se pueden abordar?
El siguiente desafío que puedes asumir es asegurarte de que el método verifyEditMode reconoce cuando editMode es true. Además, enfócate en las pruebas de acciones del componente. Considera:
Stub del Pin Service
Agrega métodos como dismiss y resolveActionServer.
Acciones de prueba en el OpenLink
Simula un evento de mouse con preventDefault.
Sumérgete y desarrolla este conjunto de pruebas para perfeccionar tu cobertura en Angular. Al abordar estos retos, mejorando tus habilidades en la implementación de tests, estás fortaleciendo la calidad global de tu aplicación. ¡Adelante, sigue aprendiendo y perfeccionando!
Pruebas Unitarias en Angular: Testeo de Componentes y Servicios
it("edit mode should be true",()=>{const verifyEditMode =spyOn(component,"verifyEditMode").and.callThrough(); fixture.ngZone.run(()=>{(<any>component).router.navigate(['app/add']); fixture.whenStable().then(()=>{expect(component.editMode).toBe(true);expect(verifyEditMode).toHaveBeenCalled();});});});
Mi código completamente funcional:
import{ async,ComponentFixture,TestBed}from'@angular/core/testing';import{ActionsComponent}from'./actions.component';import{MatBottomSheetRef}from'@angular/material';import{PinsService}from'../pins/pins.service';import{NO_ERRORS_SCHEMA}from'@angular/core';import{CUSTOM_ELEMENTS_SCHEMA}from'@angular/core';classMatBottomSheetRefStub{dismiss(){}}classPinsServiceStub{resolveActionObserver(action){}}fdescribe('ActionsComponent',()=>{letcomponent:ActionsComponent;letfixture:ComponentFixture<ActionsComponent>;beforeEach(async(()=>{TestBed.configureTestingModule({declarations:[ActionsComponent],providers:[{provide:MatBottomSheetRef,useClass:MatBottomSheetRefStub},{provide:PinsService,useClass:PinsServiceStub}],schemas:[NO_ERRORS_SCHEMA,CUSTOM_ELEMENTS_SCHEMA]}).compileComponents();}));beforeEach(()=>{ fixture =TestBed.createComponent(ActionsComponent); component = fixture.componentInstance; fixture.detectChanges();});it('should create',()=>{expect(component).toBeTruthy();});it('should execute openLink',()=>{// se crea el eventoconst event =newMouseEvent('click');// se crean los espias de las funciones que queremos comprobar que fueron llamadasconst eventSpy =spyOn(event,'preventDefault').and.callFake(()=>{});const bottomSheetRefSpy =spyOn((<any>component).bottomSheetRef,'dismiss');const pinsServiceSpy =spyOn((<any>component).pinsService,'resolveActionObserver');// la funcion que estamos probando, la espiamos sin modificar su comportamientoconst openLink =spyOn(component,'openLink').and.callThrough();// ejecutamos la funcion component.openLink(event,'send');// comprobamos los resultadosexpect(openLink).toHaveBeenCalledWith(event,'send');expect(eventSpy).toHaveBeenCalled();expect(bottomSheetRefSpy).toHaveBeenCalled();expect(pinsServiceSpy).toHaveBeenCalledWith('send');});});
En versiones mas recientes de Angular al ejecutar fixture.whenStable().then() se debe invocar el it mediante un async o sino nuestra prueba nos arrojara un warn diciendo que no tiene ningun expect.
El codigo actualizado seria asi
it("should set the edit mode to false",async(()=>{const verifyEditmode =spyOn(component,"verifyEditMode").and.callThrough(); fixture.ngZone.run(()=>{(<any>component).router.navigate(["/"]); fixture.whenStable().then(()=>{expect(component.editMode).toBeFalsy();expect(verifyEditmode).toHaveBeenCalled();});});}));
Es que como forzó las otras pruebas solo corren las forzadas,
en caso de no forzar ninguna, entonces corren todas, pero como el angular-cli te crea spec a todos los componentes por defecto, muchos te van a tirar errores o warning, entonces para evitar los errores y que sea mas limpio para el entendimiento de los demás el los ocultó forzando los que quiere correr solamente.
import{ async,ComponentFixture,TestBed}from"@angular/core/testing";import{ActionsComponent}from"./actions.component";import{MatBottomSheetRef}from"@angular/material";import{PinsService}from"../pins/pins.service";import{NO_ERRORS_SCHEMA,CUSTOM_ELEMENTS_SCHEMA}from"@angular/core";exportclassMatBottomSheetRefSTUB{dismiss(){}}exportclassPinsServiceSTUB{resolveActionObserver(action){}}fdescribe("ActionsComponent",()=>{letcomponent:ActionsComponent;letfixture:ComponentFixture<ActionsComponent>;beforeEach(async(()=>{TestBed.configureTestingModule({declarations:[ActionsComponent],providers:[{provide:MatBottomSheetRef,useClass:MatBottomSheetRefSTUB},{provide:PinsService,useClass:PinsServiceSTUB},],schemas:[NO_ERRORS_SCHEMA,CUSTOM_ELEMENTS_SCHEMA],}).compileComponents();}));beforeEach(()=>{ fixture =TestBed.createComponent(ActionsComponent); component = fixture.componentInstance; fixture.detectChanges();});it("should create",()=>{expect(component).toBeTruthy();});it("preventDefault should be called",()=>{const mouseEvent =newMouseEvent("click");const mouseEventSpy =spyOn( mouseEvent,"preventDefault").and.callFake(()=>{}); component.openLink(mouseEvent,"send");expect(mouseEventSpy).toHaveBeenCalled();});it("dismiss should be called",()=>{const mouseEvent =newMouseEvent("click");spyOn(mouseEvent,"preventDefault").and.callFake(()=>{});const bottomSheetRefSpy =spyOn((<any>component).bottomSheetRef,"dismiss"); component.openLink(mouseEvent,"send");expect(bottomSheetRefSpy).toHaveBeenCalled();});it("dismiss should be called",()=>{const mouseEvent =newMouseEvent("click");spyOn(mouseEvent,"preventDefault").and.callFake(()=>{});const pinsServiceSpy =spyOn((<any>component).pinsService,"resolveActionObserver"); component.openLink(mouseEvent,"send");expect(pinsServiceSpy).toHaveBeenCalled();});});
Esto fue lo que hice 😅. Por cierto, el casting también lo pueden hacer como (component as any).
classMatBottomSheetRefStub<T>{dismiss(){}}classPinsServiceStub{resolveActionObserver(action: string){}}describe('ActionsComponent',()=>{letcomponent:ActionsComponent;letfixture:ComponentFixture<ActionsComponent>;beforeEach(async(()=>{TestBed.configureTestingModule({declarations:[ActionsComponent],providers:[{provide:MatBottomSheetRef,useClass:MatBottomSheetRefStub},{provide:PinsService,useClass:PinsServiceStub}]}).compileComponents();}));beforeEach(()=>{ fixture =TestBed.createComponent(ActionsComponent); component = fixture.componentInstance; fixture.detectChanges();});it('should create',()=>{expect(component).toBeTruthy();});it('should open a link',()=>{const event =newMouseEvent('click');const mouseEvent =spyOn(event,'preventDefault');const dismiss =spyOn((component as any).bottomSheetRef,'dismiss');const resolve =spyOn((component as any).pinsService,'resolveActionOberver'); component.openLink(event,'ok');expect(mouseEvent).toHaveBeenCalled();expect(dismiss).toHaveBeenCalled();expect(resolve).toHaveBeenCalledWith('ok');});});
import{CUSTOM_ELEMENTS_SCHEMA,NO_ERRORS_SCHEMA}from'@angular/core';import{ async,ComponentFixture,TestBed, waitForAsync,}from'@angular/core/testing';import{MatBottomSheet}from'@angular/material/bottom-sheet';import{RouterTestingModule}from'@angular/router/testing';import{ActionsComponent}from'../actions/actions.component';import{LayoutComponent}from'./layout.component';classMatBottomSheetStub{open(){}}fdescribe('LayoutComponent',()=>{letcomponent:LayoutComponent;letfixture:ComponentFixture<LayoutComponent>;beforeEach(waitForAsync(()=>{TestBed.configureTestingModule({declarations:[LayoutComponent],providers:[{provide:MatBottomSheet,useClass:MatBottomSheetStub}],imports:[RouterTestingModule.withRoutes([{path:'',component:LayoutComponent},{path:'app/add',component:LayoutComponent},]),],schemas:[NO_ERRORS_SCHEMA,CUSTOM_ELEMENTS_SCHEMA],}).compileComponents();}));beforeEach(()=>{ fixture =TestBed.createComponent(LayoutComponent); component = fixture.componentInstance; fixture.detectChanges();});it('should create',()=>{expect(component).toBeTruthy();});it('Should set the editMode to false',()=>{const verifyEditMode =spyOn(component,'verifyEditMode').and.callThrough(); fixture.ngZone.run(()=>{(<any>component).router.navigate(['/']); fixture.whenStable().then(()=>{expect(component.editMode).toBeFalsy();expect(verifyEditMode).toHaveBeenCalled();});});});it('Should open',()=>{const open =spyOn((<any>component).bottomSheet,'open'); component.openBottomSheet();expect(open).toHaveBeenCalledWith(ActionsComponent);});it('Should set the editMode to true',()=>{const verifyEditMode =spyOn(component,'verifyEditMode').and.callThrough(); fixture.ngZone.run(()=>{(<any>component).router.navigate(['app/add']); fixture.whenStable().then(()=>{expect(component.editMode).toBeTruthy();expect(verifyEditMode).toHaveBeenCalled();});});});});