Esta es una traducción (libre) del artículo Test Isolation with React de Kent C. Dodds.
La inspiración para este post viene de ver tests de React como estos:
Así que me gustaría hablar sobre la importancia de la independencia o aislamiento de los tests y guiarte hacia una mejor manera de escribir tests que aumente su fiabilidad, simplifique el código y aumente tu confianza en los tests.
Tomemos como ejemplo este simple componente:
Aquí la versión renderizada del mismo:
Nuestra primera suite de tests
Empecemos con una primera suite de tests como la que inspiró este post:
En primer lugar, a partir de @testing-library/react@9.0.0 este estilo de testing ni siquiera funcionaría correctamente, pero imaginemos por un momento que lo hiciera.
Estos tests nos dan un 100% de cobertura de la funcionalidad del componente y verifican exactamente lo que dicen que van a verificar. El problema sería que comparten un estado mutable. Pero… ¿cuál es el estado mutable que están compartiendo? ¡El componente!
Un test hace click sobre el botón del contador y los otros se apoyan en ese hecho para pasar. Si borráramos (o nos saltáramos con [.rr-code].skip[.rr-code] el segundo test (’cuando se hace click, el contador se incrementa’) todos los siguientes tests se romperían:
Esto supone un problema puesto que estos tests no pueden ser refactorizados de manera fiable, o no es posible ejecutar un único test aislado con propósitos de debugging porque no sabemos qué tests están impactando en el funcionamiento de qué otros. Para alguien que llega con la intención de hacer algún cambio a un test puede resultar muy confuso que otros tests sin relación aparente comiencen a romperse.
Mejor
Intentemos entonces algo diferente y veamos cómo cambian las cosas:
De esta manera, cada test está completamente aislado del resto. Es posible borrar o saltar cualquiera de los tests y el resto de ellos seguirá pasando. La diferencia mayor y fundamental sería que cada uno de los tests tiene una instancia propia del contador con la que trabajar y que es desmontada después de cada test (esto ocurre automáticamente gracias a React Testing Library). Este cambio menor se traduce en una reducción significativa de la complejidad de nuestros tests.
Un argumento de algunas personas contra esta aproximación sería esgrimir que es más lenta que la aproximación anterior. No estoy totalmente seguro de cómo responder a eso… ¿Como cuánto de más lento? ¿Unos pocos milisegundos? En tal caso, ¿y qué? Entonces tu componente probablemente debería ser optimizado pues ese hecho en sí ya es un mal indicador. Sé que esta aproximación hace que se vaya acumulando tiempo en la ejecución, pero no debería importar en absoluto esperar unos cuantos segundos extra a cambio de la confianza añadida y la mejora en el mantenimiento que se proporciona. Además, a menudo no será necesario ejecutar la base de tests al completo gracias al fantástico soporte al watch mode que tiene Jest.
Todavía mejor
Aún así, no estoy del todo convencido con los tests que tenemos arriba. No soy un gran fan del [.rr-code]beforeEach[.rr-code] y el hecho de compartir variables entre tests. Siento que conducen a tests que son más difíciles de entender. Intentémoslo de nuevo:
Aquí hemos aumentado las repeticiones de código, pero ahora cada test está aislado no solo técnica sino visualmente. Puedes mirar un test y ver exactamente lo que hace sin tener que preocuparte de qué hooks se están ejecutando en su interior. Esto implica una mejora muy significativa de tu capacidad para poder refactorizar, quitar o añadir tests.
Todavía incluso mejor
Me gusta el punto al que hemos llegado, pero creo que necesitamos llevar nuestro planteamiento un paso más allá antes de quedarnos completamente satisfechos con el resultado. Hemos dividido nuestros tests por funcionalidad, pero en lo que queremos ganar confianza realmente es en el caso de uso para el que nuestro componente va a ser utilizado.
Nuestro contador permite ser clickado hasta que se alcanza un número máximo, y entonces requiere ser reseteado. Este es el caso que estamos intentando verificar y en el que queremos ganar confianza. Cuando estoy testeando, estoy mucho más interesado en casos de uso que en funcionalidad específica; así que, ¿cómo crees que cambiarían estos tests si nos preocupáramos más por el caso de uso que por la funcionalidad específica?
Yo adoro este tipo de tests, me ayuda a evitar pensar en la funcionalidad y enfocarme más en lo que estoy intentando conseguir con el componente. Además, creamos automáticamente una documentación mucho más explícita que la producida por medio de los tests anteriores.
En el pasado, la razón por la que no se hacía esto (tener múltiples aserciones dentro de un único test) era que resultaba difícil poder descubrir en qué punto del test se había producido el error. Pero hoy en día se dispone de una gestión de errores mucho más avanzada y resulta muy sencillo identificar en qué punto el test se ha roto. Por ejemplo:
Aquí el extracto de código es especialmente útil. Muestra no sólo el número de línea, sino también el código que se encuentra en torno a la aserción fallida, que incluye nuestros comentarios y más código, con el objetivo de proporcionarnos contexto en torno al mensaje de error, algo que no era posible con las primeras versiones que hemos mostrado de los tests.
Pero atención, esto no significa que no debas separar diferentes casos de uso para un componente. Hay muchas razones por las que querrías hacerlo y la mayor parte del tiempo será lo que hagas. Simplemente pon más foco en los casos de uso que en la funcionalidad y de manera automática estarás cubriendo la mayor parte del código que te concierne.
Será entonces cuando podrás añadir unos pocos tests extra para cubrir los casos límite.
Conclusión
¡Espero que te haya resultado útil este artículo! Puedes encontrar el código del ejemplo aquí. Intenta mantener tus tests aislados los unos de los otros y pon foco en los casos de uso en lugar de en la funcionalidad, ¡así tendrás una experiencia de testing mucho mejor! ¡Buena suerte y buen testing!