Este proyecto representa el clásico ejemplo del conversor de millas a kilómetros, generado con Angular CLI versión 18, y modificado luego manualmente.
Desde el raíz del proyecto, en una consola, ejecutar:
npm start
- ver cómo se implementa el conversor utilizando binding y template
- ver cómo se implementa el conversor utilizando signals
Veamos el primer test
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent]
}).compileComponents()
})
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent)
const app = fixture.componentInstance
expect(app).toBeTruthy()
})
})
En el método beforeEach inicializamos dos variables muy importantes para poder hacer pruebas
- appComponent: un NgModule mockeado por el componente de testing de Angular llamado TestBed
- componente: una referencia a nuestro componente de Angular, también mockeado
El primer test valida que el componente sea un valor posible (truthy, este test tiene sentido como smoke test de que cualquier cambio que introduzcamos no rompa el componente. Es un test básico de que la aplicación levanta.
Si queremos testear la conversión ¿cómo simulamos la carga de un valor? Tenemos que encontrar el input dentro de la página, las opciones son
- por el tag
<input>
: tiene la desventaja de que el tag HTML tiene un sentido semántico y es visible por el usuario, está sujeto a cambios. Podemos cambiar un span por un div, o un button por un anchor (a href) y eso revela la fragilidad de nuestros tests. - por un id: si bien soporta mejor el cambio estético, sigue siendo algo que el navegador renderiza en la página.
- por un atributo que tiene el tag, en este caso
data-testid
, será la opción que recomendemos nosotros, basado en esta página. El prefijodata-
hace que los navegadores lo ignoren en el render, pero los tests lo pueden utilizar. De esa manera no interfieren los atributos propios para mostrar la página vs. los atributos para testearla. Bueno, hay cierta intrusión porque si no tuviéramos tests tendríamos menos tags que leer, pero es un costo lo suficientemente justo.
Nuestro html queda:
<input data-testid="millas" name="millas" ...
Y en los tests tendremos una función de alto nivel para buscar un elemento por data-testid
:
const buscarElemento = (testId: string) => {
const compiled = appComponent.debugElement.nativeElement
return compiled.querySelector(`[data-testid="${testId}"]`)
}
El nativeElement es una simplificación del DOM que parsea el browser, y dentro de esa jerarquía de elementos visuales que tiene el DOM podemos hacer la consulta querySelector, por
- un tag específico (en este caso, 'h1')
- por un identificador (anteponiendo un numeral)
- por cualquier elemento que tenga una clase determinada (utilizando como prefijo un punto: '.')
- o bien por cualquier otro atributo, como será nuestro caso
Un detalle importante es que la función está definida dentro del contexto de los tests, por eso la referencia appComponent existe. Si la definiéramos afuera tendríamos que recibir appComponent
como parámetro de la función.
Por último, ¿qué sucede si ingresamos el valor 100? Nuestro valor esperado es 160,934. Para ello podemos
- modificar el componente de Angular (recordemos que AppComponent tiene como propiedades title y conversor que apunta a un objeto de dominio Conversor)
- o bien simular la interacción de carga de un usuario (donde ingresa el valor "100" como string en el elemento input millas, y luego dispara el botón Convertir)
El resultado se captura del DOM como vimos anteriormente. Mostramos uno de los tests:
it('conversión de millas a kilómetros exitosa con 3 decimales', () => {
const millasInput = buscarElemento('millas')
millasInput.value = 100
millasInput.dispatchEvent(new Event('input'))
const convertirButton = buscarElemento('convertir')
convertirButton.click()
appComponent.detectChanges()
const resultado = buscarElemento('kilometros')
expect(resultado.textContent).toContain('160,934')
})
En la solución encontrarás algunas abstracciones adicionales, aquí por motivos didácticos queremos concentrarnos en el flujo de interacciones:
- cargamos un valor en el componente
- luego simulamos que la persona usuaria presiona el botón "Convertir"
Para más información recomendamos leer la documentación oficial de Angular