Photo by Louis Reed on Unsplash

Pruebas Unitarias en Angular: mi caso contra TestBed

Como abandonarlo con 0 esfuerzo, mejorando tu rendimiento.

Carlos Solis
5 min readAug 18, 2020

--

Consistentemente encuentro desarrolladores de todos los niveles que odian hacer tests. La mayoría evita hacerlos, muchos los hacen el mínimo posible y los mas extremos dicen abiertamente que son una perdida de tiempo.

Pero no es así, los test unitarios son indispensables para un código de buena calidad, fácil de mantener y en especial para hacer que nuestro trabajo sea más sencillo sin tener que preocuparnos por regresiones o conductas extrañas. Personalmente creo que no puedes ser un desarrollador de nivel senior si no dominas e incluyes tests en tu código. Los tests hacen desarrolladores felices! .. o no?

Pues al menos mi vida era miserable, sabia lo importantes que eran y estaba obligado a incluir tests, pero solo pensar en hacerlo me inundaba de unas ganas incontrolables de convertirme en granjero: las vacas no necesitan pruebas unitarias para validar su leche! Mira las gallinas, siempre dan los mismos huevos consistentemente sin siquiera saber qué es un E2E.

Todo cambio hace unos años, cuando me di cuenta de dos grandes errores que cometía:

Error 1: El Angular CLI te miente

Si, el CLI ahorra miles de horas en configuración y le permitió a la comunidad de desarrolladores de Angular crear proyectos con un standard unificado que todos podemos comprender, pero tiene una mentira oculta, mira esta prueba unitaria creada automáticamente por el CLI

import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
/// ❌ Esto drena tu alma
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));

/// ❌ Este test no evalua nada realmente
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});

/// ✅ Este si es un test unitario de un elemento de la app
it(`should have as title 'mi-variable'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('mi-variable');
});

/// ❌ Esto debería ser un test de integración
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('mi-variable app is running!');
});
});

Si el CLI lo hace es porque es la mejor forma ¿no? ¡Pues no! Mira los comentarios del test y veras que esta repleto de ineficiencia y tests innecesarios.

Vamos un paso mas allá, ¿Que pasa si te digo que no necesitas del todo usar TestBed? Cero. NADA.

Mira como sería de simple si arreglamos esa prueba y eliminamos todo lo que es innecesario:

import { AppComponent } from './app.component';

describe('AppComponent', () => {

let component:AppComponent;

/// Crea una nueva instancia del componente para cada prueba
beforeEach(()=>{
component = new AppComponent();
})

/// El mismo unit test generado por el CLI, con menos código
it(`should have as title 'mi-variable'`, () => {
expect(component.title).toEqual('mi-variable');
});

});

Así es, ¡No necesitas TestBed!!! Repite conmigo: No 👏 Necesito 👏 TestBed 👏 NUNCA👏

Reemplazarlo no puede ser más fácil: sólo necesitas inicializar tu componente como cualquier clase de javascript y usar el bloque beforeEach para asegurarte que cada prueba se realiza con una instancia fresca.

Con ese mismo sistema, puedes probar cualquier componente de Angular: inyectar dependencias, eventos del ciclo de vida del componente y hasta hacer mocking de servicios. Después de probar esta técnica incluso en aplicaciones grandes con centenares de componentes y servicios, nunca he vuelto a necesitar TestBed.

Puedo escuchar desde acá algunos puristas preocupados preguntando como vamos a examinar que ese evento X se ejecute al hacer click o que ese texto se actualice cuando el usuario no valide ese formulario, eso nos lleva al segundo y mas importante error que descubrí

Error 2: Pruebas unitarias != pruebas de integración

Los unit test o pruebas unitarias existen para evaluar que cada parte del código realice lo que se desea, en el caso de angular, que los métodos de un componente hagan su trabajo correctamente, las pruebas unitarias evalúan LÓGICA.

las pruebas de integración o pruebas E2E examinan la funcionalidad en el navegador, verifican que la aplicación como un todo funcione, evalúan la FUNCIONALIDAD de los componentes integrados en una aplicación.

Para poner ejemplos concretos, los unit test evalúan que el método para validar el numero de tarjeta de crédito de el resultado correcto al insertarle un numero o que el método que envía los datos al banco devuelva un error si no logra conectarse. La prueba de integración evalúa todas las partes integradas: que desde el navegador el usuario pueda realizar una compra exitosa.

Sabiendo la diferencia entre ambos tipos de prueba y conociendo que el principal aporte de TestBed es probar el DOM de un componente, lamento decirte que:

TestBed es una pérdida de tiempo

No olvidemos nunca que Angular es Javascript(typescript) enchulado, siempre podremos hacer pruebas a la lógica javascript del componente.

Por más herramientas que tenga TestBed es irrelevante. No solo confunde la diferencia entre prueba unitaria y prueba de integración, también hace el proceso de prueba infinitamente más lento, porque tiene que hacer un render de cada plantilla antes de probarla.

TestBed también es ineficiente, supongamos que probar el DOM en pruebas unitarias es una buena idea (y no, no lo es!) en todo caso, vas a probar la plantilla de tu componente en un entorno aislado, como si fuera una clase encapsulada, pero un elemento HTML funciona totalmente distinto y estará siempre integrado con el resto de elementos del DOM, que pueden afectar su comportamiento, por ejemplo, ocultándolo o enviando eventos que generen conflictos con sus eventos internos ademas de modificar su apariencia heredándole propiedades gráficas de CSS. En otras palabras, la plantilla que se prueba en TestBed, puede tener resultados totalmente diferentes al integrarse. No sirve de nada y casi de seguro una prueba e2e tendrá que verificar el resultado final, o sea, se duplica el trabajo.

Ok, Ok, y ¿cuál es la solución?

En síntesis: deja de usar testbed, prueba exclusivamente la lógica en tus unit tests y usa pruebas de integración para todo lo que ocurra en el DOM

Entonces, ¿Qué ganamos con reducir tiempo en los unit test si al final vamos a gastar más tiempo con tests de integración?

El asunto es que son tipos diferentes de tests y ya deberías estar ejecutando ambos, eliminar TestBed solo elimina complejidad innecesaria.

Cada tipo de prueba tiene diferentes objetivos, si solo pruebas la lógica puede que se te escapen errores enormes cuando todo se ejecuta en conjunto, si solo examinas la funcionalidad, de seguro encontrarás errores misteriosos y perderás mucho tiempo depurando. Pero si los ejecutas en conjunto estás aprovechando el todo el potencial de un control cruzado de pruebas, que, aunque de ninguna forma garantiza aplicaciones perfectas, el simple hecho de incorporarlas te hace crear código de mejor calidad y reduce notablemente los problemas en tu producto final.

Para cerrar, si este articulo te motiva a comenzar a incorporar pruebas de integración o ya lo haces pero sientes que Protractor se te queda corto, cámbiate a Cypress y vuelve a disfrutar la experiencia de hacer pruebas.

--

--

Carlos Solis

Product evangelist @modyo, author & fullstack developer