El atributo data-testid
¿Qué es el atributo data-testid?
«data-testid» es el nombre del atributo que identificará un nodo del DOM durante el proceso de testeo.
Con esta definición ya podemos intuir, que únicamente tiene que servir a la hora de seleccionar un nodo por ese identificador (tiene que ser único) para hacer nuestros tests, para nada más. ¿Pero qué tests? ¿Los unitarios?¿Los end-to-end? … bueno, sigue leyendo.
Como vemos, su morfología es la siguiente:
- El primer bloque “data-*” viene gracias a la extensibilidad del HTML con la que podemos almacenar una información adicional en elementos HTML.
- El segundo bloque “testid” como su nombre indica es el nombre de identificador para tests.
Así por ejemplo podemos tener en nuestro HTML:
<div class=”postcard__title–large” data-testid=”postcard-title”> ... </div>
Ventajas
Son varias las ventajas para usar este atributo entre ellas:
Separación de conceptos
Básicamente se distingue que ese identificador es para tests y no para el código que se ejecutará en desarrollo o producción.
Fiabilidad
Al seleccionar un elemento por este atributo, será más robusto a errores si por ejemplo cambia tanto la estructura del DOM como alguna clase css si se utilizan selectores css (resistentes al cambio).
Facilidad de búsqueda
Claramente es más fácil buscar por un identificador que no por selectores complejos.
…
Más allá de su definición
Bien, hasta aquí ya tenemos aprendido el concepto de «data-testid«, pero ¿En qué se diferencia con el «id» que ya existe? ¿Cómo lo vamos a usar? ¿en qué tipo de tests? ¿Es necesario que esté en el código de producción? … son preguntas que intentaremos responder a continuación.
Diferencias con el identificador «id»
Hace tiempo ya escribimos sobre Identificadores dinámicos en componentes. Pero sean estáticos o dinámicos la diferencia está en que los «id» se utilizan básicamente para la lógica de negocio que se ejecuta en modo desarrollo o producción y no para tests.
Por ejemplo, si se necesita mostrar un mensaje o texto cuando el usuario hace clic en un botón por la lógica de negocio, el código javascript buscará ese elemento por su «id» y hará las acciones pertinentes para mostrarlo.
<span id="mensaje-aviso" style="display: none">Mensaje de aviso</span>
<button onclick="document.getElementById('mensaje-aviso').style.display = 'block'">ver mensaje</button>
Si tuviéramos que hacer un test de ese código, encontrar ese botón sería complejo dado que:
- No tiene identificador único ni una clase css por la que buscar.
- Podríamos buscar por document.getElementsByTagName, pero podrían existir varios botones en todo el documento HTML, y podría cambiar su posición/orden o cambiar su texto, …
y aún si lográramos obtener el botón correcto y lanzar un evento de clic, buscar ahora el mensaje por su identificador «mensaje-aviso» puede servir, pero ese identificador puede cambiar.
Así que para los tests deberíamos añadir el atributo «data-testid» de la siguiente manera:
<span data-testid="mensaje-aviso" id="mensaje-aviso" style="display: none">Mensaje de aviso</span>
<button data-testid="boton-mensaje" onclick="document.getElementById('mensaje-aviso').style.display = 'block'">ver mensaje</button>
De esta manera para un test, bastaría hacer a grandes rasgos las siguientes acciones:
- Buscar el botón por su «data-testid» y hacer clic.
- Buscar el mensaje de aviso por su «data-testid» y verificar que es visible.
Vemos que parece raro tener el id» y el «data-testid» del mensaje con el mismo identificador «mensaje-aviso» y lo primero que se te pasa por la cabeza es que es redundante. Nada más lejos, que aunque en el ejemplo tengan el mismo valor, en algún momento podría cambiar el «id» del mensaje de texto y a los tests no les afectaría.
¿Para qué tipo de tests?
Bien, pues sirven tanto para los tests unitarios de componentes como para los tests end-to-end (e2e).
Para los unitarios, aunque el marcado del componente sea un simple elemento HTML «div», le añadiremos su «data-testid» porque sabemos que puede evolucionar y volverse más complejo. De esta manera no deberíamos tocar apenas nada.Así, existen librerías de testing como testing library que nos facilita la búsqueda de un elemento mediante el método “getByTestId”.
Para los tests end-to-end (e2e), he visto casos donde se buscan elementos no solo por «id» si no aún peor, por clases css ó estructura DOM (expresiones XPath) y se rompían cada vez que algo cambiaba. Por lo tanto, son necesarios también para este tipo de tests.
¿Pueden estar los data-testid en producción?
Es una buena pregunta, dado que en producción solo es necesario el código final, no se hacen tests y de ahí esa duda.
Habitualmente los tests e2e se lanzan contra algún entorno de desarrollo o de pre-producción y ahí obviamente deberían estar los «data-testid«, pero en el entorno de producción no son necesarios (a no ser que se quieran lanzar también esos tests). Que no sean necesarios no significa que no puedan estar, pero sí que causa ruido en el HTML final además de inflar, aunque poco, el peso final del bundle o se le facilita la vida a un atacante.
¿Cómo puedo quitar el data-testid en producción?
Hay varias técnicas para quitarlos, como:
- El uso de directivas, pero que influirían en el rendimiento general de la aplicación.
- Modificar el proceso de build (webpack) para que los elimine,
- O bien algún package externo pueda hacernos el trabajo.
Buscar por otro atributo que no sea data-testid con Testing Library
¿Utilizas «Testing Library»? Pues ahora puedes buscar tus identificadores por otro atributo que no sea «data-testid» tal y como puedes leer en su web «Overriding data-testid«.
Esto es si por ejemplo quieres buscar por el típico «id» o bien por «testID» puedes hacerlo cambiando la configuración a la hora de ejecutar tus tests, solo hace falta importar el módulo configure y pasarle un objeto de configuración diciéndoselo tal como se comenta en su documentación: configure({testIdAttribute: 'data-my-test-attribute'})
Aunque en el momento de ampliar este artículo con el ejemplo de la documentación produce un error (quizás según la versión de la librería) y la estructura del objeto del «configure» sería en Angular de la siguiente manera: configure({ dom: { testIdAttribute: 'data-my-test-attribute' } })
Ejemplo si el atributo es «af-id»:
*.html:
<div af-id="example1"></div>
*.spec.ts:
import { configure, screen } from '@testing-library/angular';
configure({ dom: { testIdAttribute: 'af-id' } });
...
expect(screen.getByTestId('example1')).toBeInTheDocument();
...