Aprendiendo acerca del testing en nuestras aplicaciones web

0
372

Estos días he estado leyendo en web.dev una sección que han sacado hablando del testing, por lo que he decidido hacer un artículo en el que hablo un poco de los conceptos con los que me he quedado a nivel teórico del mismo.

Índice de contenidos

  1. ¿Que son los test?
  2. Test manuales y automáticos
  3. Tipos de test automáticos
  4. Cobertura de código
  5. Estrategias de testing
  6. Pirámide de test
  7. Adaptaciones de la pirámide de test
  8. Enfoques más centrados en la interfaz de usuario
  9. ¿Que estrategia debería elegir?
  10. ¿Por donde debería empezar?
  11. Conclusiones

¿Que son los test?

Al escribir software, la manera en que podemos confirmar que funciona correctamente es mediante los test. Los test se pueden definir como el proceso en el que ejecutamos software de maneras específicas para asegurarnos de que este se comporte de la manera que esperamos.

¿Que conseguimos con ellos?

Un test exitoso puede darnos la confianza de que, al agregar nuevo código, funciones, actualizar dependencias, realizar un refactor o cualquier otro cambio, el código existente seguirá funcionando como esperamos. Los test también pueden ayudar a proteger nuestro código contra escenarios poco comunes o entradas inesperadas.

Algunos ejemplos de comportamientos en la web que podrías querer probar incluyen:

  • Asegurarse de que una función de un sitio web opere correctamente cuando se hace clic en un botón.
  • Confirmar que una función compleja produce los resultados correctos.
  • Completar una acción que requiere iniciar sesión de usuario.
  • Verificar que un formulario informe adecuadamente un error cuando se ingresan datos mal formados.
  • Asegurarse de que una aplicación web compleja continúe funcionando cuando un usuario tiene ancho de banda extremadamente bajo o está sin conexión.

Test manuales y automáticos

Existen dos dos tipos de test, que serían los manuales y automatizadas.

  • Los test manuales implican que sean los humanos quienes ejecuten el software directamente, por ejemplo, cargando un sitio web en el navegador y confirmando que se comporta como esperamos. El problema de este tipo de test es que cada ejecución consume una enorme cantidad de tiempo. A pesar de que los seres humanos somos muy creativos, y podemos realizar un tipo de prueba conocida como pruebas exploratorias, donde exploramos la aplicación en búsqueda de errores ocultos, aún podemos ser deficientes a la hora de notar fallos o inconsistencias, especialmente al realizar la misma tarea muchas veces.
  • Los test automáticos consisten en la aplicación de herramientas de software para automatizar el proceso manual de revisión y validación de un producto de software que lleva a cabo una persona, evitando que la misma realice pasos repetitivos, como configuración o verificación de resultados. Es importante resaltar que, una vez establecidos los test automáticos, estos se pueden realizar con regularidad. Es crucial saber que existen diferentes tipos de test automáticos, de los que hablaremos en las siguientes secciones.

Los test manuales tienen su lugar, a menudo como un precursor para escribir test automáticos, pero también cuando los test automáticos se vuelven poco confiables, demasiado amplias en alcance o difíciles de escribir.

Tipos de test automáticos

En esta sección hablare de los tipos de test automáticos más conocidos junto a otros pocos. Estos no son los únicos tipos de test que existen, pueden haber muchos tipos más, dependiendo de como se quieran plantear dentro de tu aplicación.

Test unitarios

Los test unitarios se centran en evaluar partes o unidades pequeñas y fácilmente testables de una aplicación de manera individual e independiente para garantizar su correcto funcionamiento. Estas unidades pueden variar desde funciones y clases hasta servicios o componentes completos. Las características clave de los test unitarios incluyen una ejecución rápida, el aislamiento y una sencilla capacidad de mantenimiento.

Este tipo de test se realiza de manera independiente de otras partes del sistema e, idealmente, no tiene dependencias externas, como la obtención de datos de una red o el uso implícito de otras funciones o bibliotecas. Los tests unitarios son esenciales para garantizar la calidad y la robustez del código, ya que permiten identificar y corregir errores en unidades de código pequeñas antes de que se integren en el sistema completo.

Test de integración

Los test de integración se centran en las interacciones entre componentes o sistemas, es decir, en cómo trabajan juntos.

Esto incluso puede incluir "dependencias reales", como una base de datos externa en modo de prueba, en lugar de hacer un mock de las mismas.

Al abordar escenarios más amplios que los test unitarios, los tests de integración contribuyen a mantener la estabilidad y calidad del software, asegurando que todas las partes del sistema funcionen armoniosamente como una entidad coherente.

Test extremo a extremo (E2E)

Este tipo de test a menudo se llaman test de interfaz de usuario (UI), y es un nombre explica aún mejor su función. En este tipo de test se interactúa con la interfaz de usuario de tu aplicación, incluida la pila completa de la aplicación, y prueban tu aplicación de un extremo a otro.

Describen una interacción completa con tu aplicación web o sitio web, tal vez centrada en una característica específica, y generalmente se ejecutan dentro de un navegador controlado por un agente que simula a un usuario y sus interacciones. Los test de extremo a extremo requieren más tiempo de ejecución ya que involucran todo el sistema, y más tiempo de ejecución requiere más potencia de cómputo. Como resultado, este esfuerzo adicional se traduce en mayores costos de mantenimiento.

Test visuales de interfaz de usuario

El test en cuestión captura dos imágenes: una después de un cambio y otra que muestra el «estado actual». Estos resultados se entregan a un revisor humano para su inspección y verificación. En esencia, esta prueba ayuda a identificar posibles «errores visuales» en la apariencia de una página, yendo más allá de los errores puramente funcionales que no se expresan explícitamente en las afirmaciones del código.

Test de API

Los test de API pueden referirse a confirmar el comportamiento de las API que proporciona tu software, o acceder a API del mundo real para confirmar su comportamiento. De cualquier manera, tienden a probar las abstracciones entre sistemas, es decir, cómo se comunicarán eventualmente entre sí, sin integrarlos realmente como en una prueba de integración.

Estas pruebas pueden proporcionar un precursor básico para los test de integración sin la sobrecarga de ejecutar los sistemas entre los que estás probando las conexiones. Sin embargo, los test de sistemas del mundo real pueden ser inconsistentes.

Análisis estático

No es un tipo de test en el sentido convencional, pero se tratas de un aspecto esencial en las estrategias de aseguramiento de calidad. Este tipo de análisis que se centra en la verificación automática del código sin necesidad de ejecutarlo. Ayuda a identificar problemas, errores o posibles problemas en el código fuente al analizar el propio código. El objetivo es detectar problemas temprano en el proceso de desarrollo, antes de ejecutar el código, para prevenir posibles errores y mejorar la calidad general del código.

En el contexto del análisis estático, herramientas como linters, compiladores y comprobadores de tipos juegan un papel crucial. Estas herramientas pueden analizar el código en busca de errores de sintaxis, violaciones de estilo de codificación o problemas relacionados con el tipo. La verificación de tipos de TypeScript es un buen ejemplo de análisis estático, proporcionando a los desarrolladores retroalimentación inmediata sobre posibles errores relacionados con el tipo en su código.

Cobertura de código

Es posible medir qué porcentaje de tu código está probado por pruebas automatizadas y reportar esto como una estadística a lo largo del tiempo. No recomendamos apuntar al 100% de cobertura de código, ya que eso puede llevar a una sobrecarga innecesaria, así como a pruebas simplistas o mal diseñadas que no cubren a fondo los casos de uso principales.

La cobertura en sí misma también puede ser una herramienta útil al escribir o trabajar en tus pruebas, especialmente los test de integración. Al mostrar un porcentaje o un desglose línea por línea de qué código está probado por una sola prueba, puedes obtener información sobre lo que falta o lo que se puede probar a continuación.

Estrategias de testing

En la búsqueda de una estrategia de testing efectiva, es crucial entender los objetivos y necesidades específicas de tu aplicación. Antes de sumergirse en la creación de pruebas, es fundamental definir claramente el propósito de las pruebas y establecer criterios claros para considerar que la aplicación ha sido probada de manera suficiente.

A menudo, se asume que alcanzar una amplia cobertura de pruebas es la meta final para los desarrolladores, pero este enfoque puede no ser siempre el más adecuado. Es vital considerar otro factor determinante al definir tu estrategia de pruebas: satisfacer las necesidades de los usuarios. Los usuarios confían en que las aplicaciones simplemente funcionen, y como desarrolladores, debemos esforzarnos por cumplir con esa confianza.

La elección de la estrategia de testing adecuada dependerá de la naturaleza y los requisitos de tu aplicación, así como del equilibrio entre cobertura y la satisfacción de las expectativas de los usuarios. En esta sección hablaré de los diferentes tipos de estrategias de testing que se han planteado a lo largo del tiempo.

Test Pyramid

Tan pronto como empecéis a buscar por diferentes estrategias de testing, en algún momento os encontrareis con esta estrategia como la primera de todas.

imagen de pirámide de test

Como se muestra en la imagen anterior, la pirámide de pruebas consta de tres capas:

  1. Unitarios. Estos test se encuentran en la capa base de la pirámide porque son rápidos de ejecutar y simples de mantener. Están aislados y se centran en las unidades de prueba más pequeñas.
  2. Integración. Estos test se encuentran en el centro de la pirámide, ya que tienen una velocidad de ejecución aceptable y te acercan más al usuario que de lo que podrían hacer los test unitarios.
  3. Test de extremo a extremo (E2E). Estos test simulan a un usuario genuino y sus interacciones. Estas pruebas necesitan más tiempo para ejecutarse y, por lo tanto, son más costosas. Se encuentran en la parte superior de la pirámide.

Confianza vs recursos

El orden de las capas no es una coincidencia. Muestran las prioridades y los costos correspondientes. Esto te proporciona una imagen de cuántos test deberías escribir para cada capa.

Debido a que los test E2E están más cerca de tus usuarios, te brindan la mayor confianza de que tu aplicación está funcionando según lo previsto. Sin embargo, requieren una pila de aplicación completa y un usuario simulado, por lo tanto, también son potencialmente las más costosas. Así que la confianza está en competencia directa con los recursos que necesitas para ejecutar las pruebas.

imagen de relación coste confianza de la pirámide de test

La estrategia de la pirámide de test intenta resolver este problema al hacer que te enfoques más en los test unitarios y priorices estrictamente los casos cubiertos por las pruebas E2E. Por ejemplo, tus recorridos de usuario más cruciales o los lugares más vulnerables a defectos.

Adaptaciones de la Test Pyramid

Durante varios años, las discusiones han girado en torno a la pirámide. Se ha señalado que la pirámide simplifica en exceso las estrategias de pruebas, omite muchos tipos de pruebas y ya no se ajusta a todos los proyectos del mundo real. Por lo que puede llegar a confundir. Guillermo Rauch, CEO de Vercel, tiene una opinión con respecto a esto:

Write tests. Not too many. Mostly integration.

Es una de las citas más comúnmente citadas en este tema, así que desglosemos sus puntos clave:

  • "Escribe pruebas". No solo porque construye confianza, sino también porque ahorra tiempo en el mantenimiento.
  • "No demasiadas". El 100% de cobertura no siempre es bueno, ya que entonces tus pruebas no están priorizadas y se gastarán muchos recursos en el mantenimiento.
  • "Principalmente integración". Nuevamente, se hace hincapié en los test de integración: tienen el mayor valor comercial al brindarte un alto nivel de confianza diaria manteniendo un tiempo de ejecución razonable.

Este planteamiento te hace reconsiderar la pirámide de pruebas y darle otro enfoque priorizando los test de integración. En los últimos años, se han propuesto muchas adaptaciones, así que veamos las más comunes.

Test Diamond

En esta primera adaptación reducimos la presencia de test unitarios en la pirámide de pruebas. Imaginad que vuestro proyecto ha alcanzado el 100% de cobertura en los test unitarios. Sin embargo, la próxima vez que hagamos un refactor, tendremos que actualizar muchas de estos test unitarias y podríamos sentir la tentación de omitirlas.

Como resultado, y junto con un mayor enfoque en los test de integración, puede surgir la siguiente forma:

imagen de test diamond

Una pirámide evoluciona hacia un diamante. Puedes ver las tres capas anteriores, pero con un tamaño diferente:

  • Unitarios. Escribe test unitarios como se definieron anteriormente. Sin embargo, debido a que tienden a erosionarse, da prioridad y cubre solo los casos más críticos.
  • Integración. Los test de integración que conoces, probando la combinación de unidades individuales.
  • E2E. Esta capa maneja los test de interfaz de usuario de manera similar a la pirámide de pruebas. Deberían de escribirse solo test E2E para los casos de prueba más críticos.

Testing Honeycomb

Existe otra variante, inspirada por Spotify, que se asemeja al concepto de la pirámide de pruebas, pero está especialmente diseñada para entornos de software basados en microservicios. Se denomina el «panal de pruebas», y esta analogía visual destaca la granularidad, alcance y cantidad de pruebas necesarias para un sistema de software que sigue la arquitectura de microservicios. En este contexto, donde la complejidad principal de un microservicio radica en su interacción con otros servicios, la estrategia de pruebas debería enfocarse principalmente en los test de integración.

imagen de testing honeycomb

Esta forma nos recuerda a un panal de abejas, de ahí el nombre. Tiene las siguientes capas:

  • Pruebas integradas. Siguiendo la definición de J. B. Rainsberger, estas pruebas evalúan la corrección de un sistema dependiendo del comportamiento de otro sistema. Al tener dependencias externas, deben utilizarse con precaución, reservándolas para los casos esenciales.
  • Test de integración. Esta capa es central en la estrategia. Aquí, las pruebas verifican la corrección del servicio de forma más aislada pero aún en conjunto con otros servicios. Incluyen pruebas que abordan la interacción entre sistemas, como los test de API.
  • Pruebas en detalles de implementación. Estas pruebas se asemejan a los test unitarios, pruebas que se centran en partes del código que están naturalmente aisladas y, por lo tanto, tienen su propia complejidad interna.

Testing Trophy

Ya hemos visto en las adaptaciones anteriores el hincapié que se hace en los test de integración. No obstante, hay otro tipo de prueba mencionado en el artículo anterior que, aunque no se aborda explícitamente como una prueba en términos teóricos, sigue siendo un aspecto crucial a considerar en cualquier estrategia de pruebas. Se trata del análisis estático, un elemento que no se incluye en la representación visual de la pirámide de pruebas ni en la mayoría de las adaptaciones a la misma que has visto hasta ahora.

Existe una adaptación de la pirámide de pruebas que tiene en cuenta el análisis estático mientras mantiene el enfoque en los test de integración, con el nombre de trofeo de pruebas. El trofeo de pruebas se originó en base a en la cita anterior de Guillermo Rauch y fue desarrollado por Kent C. Dodds:

imagen de pirámide de testing trophy

El trofeo de pruebas es una estrategia que representa la granularidad de las pruebas de una manera ligeramente diferente. Tiene cuatro capas:

  • Análisis estático. Juega un papel vital en esta estrategia y te permite detectar errores tipográficos, errores de estilo y otros bugs simplemente ejecutando los pasos de depuración ya esbozados.
  • Test unitarios. Aseguran que tu unidad más pequeña se pruebe adecuadamente, pero el trofeo de pruebas no los enfatiza tanto como la pirámide de pruebas.
  • Integración. Este es el enfoque principal, ya que equilibra el costo y la mayor confianza de la mejor manera, al igual que con otras adaptaciones.
  • Test de interfaz de usuario. Incluyendo pruebas E2E y pruebas visuales, están en la parte superior del trofeo de pruebas, similar a su papel en la pirámide de pruebas.

Por aquí os dejo una publicación de Kent C. Dodds sobre esta estrategia.

Enfoques más centrados en la interfaz de usuario

Si bien la automatización de pruebas es valiosa, es importante recordar que los test manuales siguen siendo esenciales. Los test automatizadas deberían aliviar las tareas rutinarias y liberar a los ingenieros del aseguramiento de calidad para que se centren en áreas cruciales. En lugar de reemplazar los test manuales, la automatización debería complementarlas.

En esta sección vamos a ver dos adaptaciones de la pirámide de pruebas que se centran más en la interfaz de usuario, en donde integramos las pruebas manuales con la automatización. Ambas tienen la ventaja de una alta confianza, pero son naturalmente más costosas debido a la ejecución más lenta de las pruebas.

Test Ice Cone

El cono de hielo de pruebas, se parece a la pirámide invertida, añadiendo la capa de los test manuales.

imagen de testing ice cone

El cono de hielo tiene un mayor enfoque en los test manuales o de interfaz de usuario y el menor enfoque en los test unitarias. Esta estrategia se considera un anti-patrón y con razón. Es costoso en términos de recursos y trabajo manual.

Testing Crab

Esta estrategia es similar al cono de hielo de pruebas, pero con más énfasis en los test E2E y visuales:

imagen del testing crab

Esta estrategia de testing incluye un aspecto adicional: verifica que tu aplicación funcione bien y tenga una buena apariencia. El cangrejo de pruebas destaca la importancia de los test visuales. Los test de integración, divididas en test de componentes y API, pasan a un segundo plano, y los test unitarios desempeñan un papel aún más secundario aquí.

Aunque estas dos estrategias de pruebas son más costosas, tienen su lugar, por ejemplo, en proyectos más pequeños que requieren menos pruebas y no necesitan cubrir tanta complejidad. En este caso, una estrategia de pruebas a gran escala centrada en los test de integración podría ser excesivamente compleja.

¿Que estrategia debería elegir?

Tras leer la sección anterior y ver la gran cantidad de estrategias que podemos plantear en nuestros proyectos, nos puede surgir la duda de cual podría ser la más adecuada. Desde el artículo de web.dev han mostrado una tabla resumen que en base al tamaño de la aplicación, la composición del equipo, la dependencia de los test manuales nos recomienda una estrategia de testing:

Tamaño de la Aplicación Composición del Equipo Dependencia de Test Manuales Estrategia de Testing
Pequeña Solo desarrolladores Alta Test Ice Cone, Testing Crab
Pequeña Desarrolladores e ingenieros de QA Alta Test Ice Cone, Testing Crab
Pequeña Solo desarrolladores Baja Test Pyramid
Grande Solo desarrolladores Alta Testing Trophy, Test Diamond
Grande Desarrolladores e ingenieros de QA Alta Testing Trophy, Testing Crab
Grande Solo desarrolladores Baja Testing Trophy, Testing Honeycomb

Estas recomendaciones son generales y pueden ajustarse según las circunstancias y preferencias específicas del equipo.

¿Por donde debería empezar?

Es un poco complicado al principio definir por donde deberíamos empezar a la hora de incluir test en nuestra aplicación, o que deberíamos testear y con que tipo de test. Mientras buscaba información para redactar este tutorial me encontré con un artículo en el que Kent C. Dodds daba su opinión de como saber qué se debería testear, y me encuentro bastante de acuerdo con los puntos que explica en el mismo.

Lo que plantea en su artículo es que al principio del todo hagamos una lista de las funcionalidades que va a tener nuestra aplicación, una vez hecho esto, ordenarlas en base a importancia y priorizarlas.

Una vez que cuentes con la lista de las funcionalidades, se debería crear un test E2E de lo que vendría a ser el “camino feliz” de cada una de ellas, que sería replicando el camino que seguiría la mayoría de usuarios para realizar las acciones. Estos test no te van a servir para cubrir todos los posibles casos de nuestra aplicación, ni deberían, ya que son muy costosos.

Una vez hechos los test E2E, deberías empezar con los test de integración, donde ya harías test de casos más complicados. Luego para código relacionado con la lógica de negocio que utilizan nuestras funcionalidades, optaríamos por los test unitarios.

El objetivo no es cubrir el 100% del coverage en nuestra aplicación, ya que esto tomaría bastante tiempo en caso de que se hiciera cualquier tipo de cambio y realmente no es tan necesario.

Conclusiones

En este tutorial se ha hablado de los test a un nivel más teórico, donde hemos visto para que son necesarios, los diferentes tipos que existen de los mismos y las estrategias que podemos seguir dependiendo de nuestro proyecto entre otras cosas. Me parece que es importante conocer toda esta información y entender los conceptos detrás de esta para poder ser capaces de generar de tener un código que te aporte calidad y seguridad.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad