Angular usa Jasmine como framework de pruebas y Karma como test Runner. A partir de la versión 15 de Angular se eliminaron algunos archivos de configuración para hacer más fácil el entendimiento del framework para nuevos desarrolladores. Karma.conf.js ya no se crea por default con el ng new, sin embargo lo podemos agregar con el siguiente comando:
ng generate config karma
Automáticamente karma va a leer todos los archivos del proyecto que acaben con spec.ts para ejecutar las pruebas.
describe - define una suite de tests. Una colección de tests. Recibe dos parámetros, un string con el nombre de la suite y una function() donde se definen los tests.
it - define un test en particular. Recibe como parámetro el nombre del test y una función a ejecutar por el test.
expect - Lo que esperar recibir ese test. Con expect se hace la comprobación del test.
Se puede seguir unos pasos muy utiles
Arrange:(Arreglar). Se establece el estado inicial, conocida como el sujeto a probar. Aquí se inicializan variables, importaciones. Se crea el ambiente a probar.
Act (Actuar): Se generan acciones o estímulos. Se llaman métodos, o se simulan clicks por ejemplo
Assert (Afirmar): observar el comportamiento. Los resultados son los esperados. Eje: Que algo cambie, se incremente, o no suceda nada.
import { Calculator } from'./calculator';describe('Test for Calculator', () => {it('#multiply should return a nine', () => {//Arrangeconstcalculator=newCalculator();//Actconstrta=calculator.multiply(3,3);//Assertexpect(rta).toEqual(9); });});
Para poder generar un reporte de covertura (más visual y específico) que no estará en modo escucha continua, lo que debe hacer es correr el sigueinte comando en la términal
ng test --no-watch --code-coverage
Con esto podemos tener mas detalle, por ejemplo de que funciones no tienen pruebas etc.
Se pueden enfocar en ciertas pruebas u omitir agregando la letra x o f al inicio de las palabras claves de los test.
con fdescribeejecuta únicamente el suite de test
con xdescribe se omite el suite de test
con fit ejecuta el focus sobre un test
con xit se omite un test
Umbral mínimo de covertura
En el archivo karma.conf.js se agrega lo siguiente para actualizarlo y saber qué necesita ser cubierto con pruebas de acuerdo al umbral.
// value.service.spec.tsdescribe('ValueService', () => {let service:ValueService;// Cada que corre una prueba se ejecutabeforeEach(() => { service =newValueService(); });// Es recomendable primero validar si el servicio fue creado de una manera exitosait('should be create', () => {expect(service).toBeTruthy(); });describe('Tests for getValue', () => {it('should return "my value"', () => {expect(service.getValue()).toBe('my value'); }); });describe('Tests for setValue', () => {it('should change the value', () => {expect(service.getValue()).toBe('my value');service.setValue('change');expect(service.getValue()).toBe('change'); }); });describe('Tests for getPromiseValue', () => {it('should return "promise value" from promise with then', (done) => {service.getPromiseValue().then((value) => {// assertexpect(value).toBe('promise value');done(); }); });it('should return "promise value" from promise using async',async () => {constrta=awaitservice.getPromiseValue();expect(rta).toBe('promise value'); }); });});
Cada que colocamos un it estamos indicando un escenario de prueba, cada escenario de prueba debería manejarse de manera insolada. Esto significa que una prueba no debería afectar a la siguiente o a otra prueba.
Lo que hacemos es crear las instancias que ocupamos para cada prueba, por ejemplo la creación del servicio. Para optimizar el código se usa la función beforeEach que va a ejecutarse antes de cada prueba.
Cada vez que se corra una funcion asíncrona dentro de un test y se resuelva la promesa dentro de una función then(), por ejemplo, se recibirá una funcion para indicar manualmente que ha terminado la ejecución. Generalmenet se usa done(). Aunque lo recomendable es usar async/await.
Test a Servicios con Dependencias
Recordar que si vamos a testear un servicio que tiene dependencias, nosotros no debemos usar las dependencias reales, es decir, ya que no le corresponde a nuestra prueba que las dependencias nos devuelval los valores deseados, nosotros solo debemos encargarnos de que nuestra prueba funcione, entonces podriamos hacer un mock de datos fake para completar nuestra prueba.
El resto de dependencias tendrán sus propias pruebas ya, así que no nos deben de importar para la nuestra. Ejemplo:
Para aislar completamente estas pruebas usamos el siguiente código.
describe('MasterService', () => {it('should be return "other value" from the fake service', () => {constfakeValueService=newFakeValueService();constmasterService=newMasterService(fakeValueService asunknownasValueService);expect(masterService.getValue()).toBe('fake value'); });});/* Este código no sería el correctodescribe('MasterService', () => { it('should be return "my value" from the real service', () => { const valueService = new ValueService(); const masterService = new MasterService(valueService); expect(masterService.getValue()).toBe('my value'); });});*/
Aunque esto es una solución un poco mas correcta, en custion de escalabilidad y mantenimiento puede ser un problema al largo plazo.
Otra forma de hacerlo es con un objeto, por que los objetos en JS funcionan casi como clases por lo que puedo hacer que se pase directamente con un objeto en master.service.spec.ts.
...it('should be return "other value" from the fake object', () => {constfake= {getValue: () =>'fake from obj'};constmasterService=newMasterService(fake asValueService);expect(masterService.getValue()).toBe('fake from obj'); });...
Esto es especialemente util al momento de probar funcionalidades que por ejemplo se conectan a una API como google maps, etc.
Spies
Para mejorar el tema del acceso a las dependencias que pueda tener un servicio podemos usar herramientas con las que cuenta Jasmine, como Spy.
Un Spy permite interceptar una función y trackear las llamadas a esta y sus argumentos, no necesariamente para obtener el valor que devuelve la función. Estos Spies solo existen en el bloque describe o it en el que fueron definidos, serán removidos luego de su implementación. Podemos definir que hara el Spy luego de ser invocado con and.
...it('should call to getValue from ValueService', () => {constvalueServiceSpy=jasmine.createSpyObj('ValueService', ['getValue']);valueServiceSpy.getValue.and.returnValue('fake value');constmasterService=newMasterService(valueServiceSpy);expect(masterService.getValue()).toBe('fake value'); // ok// verifica si realmente se ejecuto la variableexpect(valueServiceSpy.getValue).toHaveBeenCalled();expect(valueServiceSpy.getValue).toHaveBeenCalledTimes(1); });...
En este contexto nosotros estamos espiando un servicio que está siendo llamado desde el servicio que estamos probando que es el de MasterService.
Mocking
Son objetos simulados (pseudo-objetos, mock object, objetos de pega) a los que imitan el comportamiento de objetos reales de una forma controlada
Para este ejemplo usamos el concepto de mocking con jasmine a través de sus spies.
TestBed
Angular tiene una suit que nos permite hacer pruebas en un entorno de una manera más sencilla.
Con esto se obtiene un contexto más limpio ya que estamos usando el patrón de inyección de dependencias. Los providers ayudan a aislar cierto servicio, componente etc. Para ser probado, ahi solo debemos meter los módulos que estemos testeando.
TestBed unido con los spies nos ayudara para resolver la inyección de dependencias. Tomando como ejemplo a master service
La responsabilidad del API donde se conecta el frontend es del backend, es decir, para las pruebas unitarias no debemos tener en cuenta si el servidor está corriendo o no.
HttpClientTestingModule es un módulo de Angular que se utiliza para realizar pruebas unitarias de componentes y servicios que utilizan HttpClient, que es la clase de Angular encargada de realizar solicitudes HTTP.
Al hacer una configuración http podemos decirle a la prueba que los valores los tome de un mock de datos en lugar de hacer una petición al servidor real. para este caso usamos HttpTestingController.
Por defecto Angular crea un componente parecido a este:
import { ComponentFixture, TestBed } from'@angular/core/testing';import { SearchComponent } from'./search.component';import { FormsModule } from'@angular/forms';fdescribe('SearchComponent', () => {let component:SearchComponent;let fixture:ComponentFixture<SearchComponent>;beforeEach(async () => {awaitTestBed.configureTestingModule({ imports: [FormsModule], declarations: [SearchComponent], }).compileComponents(); fixture =TestBed.createComponent(SearchComponent); component =fixture.componentInstance;fixture.detectChanges(); });it('should create', () => {expect(component).toBeTruthy(); });// Utilizando el artefacto fixtureit('should have a <input> tag with a placeholder: Search for...', () => {constsearchEl=fixture.nativeElement;constinputEl=searchEl.querySelector('input');expect(inputEl).toBeTruthy();expect(inputEl.placeholder).toEqual('Search for...');// Agnóstico a la plataformaconstsearchDebug=fixture.debugElementconstinputDebug=searchDebug.query(By.css('input'));constinputEl=inputDebug.nativeElement;expect(inputEl).toBeTruthy();expect(inputEl.placeholder).toEqual('Search for...'); });});
Es muy similar al .spec al de los servicios pero tiene ciertas diferencias.
Con ComponentFixture tenemos un ambiente que nos Provee Angular para probar el componente, es la manera en que es renderizado y por tanto creado, de ese modo podemos utilizarlo. Con ayuda del artefacto fixture Angular creará el componente para obtener todos sus métodos.
Al igual que con las pruebas de servicios también necesitamos un módulo pequeño, para preparar nuestras pruebas usando TestBed.configureTestingModule (aunque de manera asíncrona).
Si queremos que la prueba a los elementos sea agnóstica a la plataforma debemos utilizar fixture.debugElement.
Cuando corremos en el navegador la aplicación de manera normal, Agular detecta los cambios en automático, sin embargo, en modo pruebas debemos utilizar fixture.detectChanges(); cada que realizamos un cambio dentro del componente, por ejemplo, la detección de un @Input().