Una estrategia efectiva para la prueba de código automatizada es extremadamente importante para garantizar el trabajo rápido y de alta calidad de los equipos de programadores involucrados en el soporte y desarrollo de proyectos web. El autor del artículo dice que en la empresa
StackPath , en la que trabaja, ahora todo funciona
bien con las pruebas. Tienen muchas herramientas para verificar el código. Pero de tal variedad, debe elegir cuál es el más adecuado para cada caso. Este es un tema aparte. Y después de seleccionar las herramientas necesarias, aún debe tomar una decisión sobre el orden de su uso.

El autor del artículo dice que StackPath está satisfecho con el nivel de confianza en la calidad del código que se logró gracias al sistema de prueba aplicado. Aquí quiere compartir una descripción de los principios de prueba desarrollados por la compañía y hablar sobre las herramientas utilizadas.
Principios de prueba
Antes de hablar sobre herramientas específicas, vale la pena pensar en la respuesta a la pregunta de qué son buenas pruebas. Antes de comenzar a trabajar en nuestro
portal para clientes, formulamos y escribimos los principios que nos gustaría seguir al crear pruebas. Lo que hicimos en primer lugar es exactamente lo que nos ayudó con la elección de las herramientas.
Aquí están los cuatro principios en cuestión.
▍ Principio número 1. Las pruebas deben entenderse como tareas de optimización
Una estrategia de prueba efectiva es resolver el problema de maximizar un cierto valor (en este caso, el nivel de confianza de que la aplicación funcionará correctamente) y minimizar ciertos costos (aquí los "costos" están representados por el tiempo requerido para soportar y ejecutar las pruebas). Al escribir pruebas, a menudo hacemos las siguientes preguntas relacionadas con el principio descrito anteriormente:
- ¿Cuál es la probabilidad de que esta prueba encuentre un error?
- ¿Esta prueba mejora nuestro sistema de prueba y los costos de los recursos necesarios para escribirla valen los beneficios derivados de ella?
- ¿Es posible obtener el mismo nivel de confianza en la entidad que se está probando que esta prueba proporciona creando otra prueba que sea más fácil de escribir, mantener y ejecutar?
▍ Principio No. 2. Se debe evitar el uso excesivo de mox.
Una de mis explicaciones favoritas del concepto de "mok" se dio en
esta presentación de la conferencia Assert.js 2018. El orador abrió la pregunta más profundamente de lo que voy a abrir aquí. En el discurso, la creación de mokas se compara con "perforar agujeros en la realidad". Y creo que esta es una forma muy visual de percibir a los moks. Aunque hay mokas en nuestras pruebas, comparamos la disminución en el "costo" de las pruebas que proporcionan los mokas debido a la simplificación del proceso de redacción y ejecución de pruebas, con la disminución en el valor de las pruebas que hace que se haga otro agujero en la realidad.
Anteriormente, nuestros programadores dependían en gran medida de las pruebas unitarias escritas para que todas las dependencias secundarias fueran reemplazadas por mokas utilizando la API de representación de
enzimas poco profundas. Las entidades representadas de esta manera se verificaron utilizando instantáneas de
Jest . Todas esas pruebas fueron escritas usando un patrón similar:
it('renders ', () => { const wrapper = shallow();
Estas pruebas están llenas de realidad en muchos lugares. Este enfoque hace que sea muy fácil lograr una cobertura de código del 100% con las pruebas. Al escribir tales pruebas, debe pensar muy poco, pero si no verifica todos los numerosos puntos de integración, tales pruebas no son particularmente valiosas. Todas las pruebas pueden completarse con éxito, pero esto no da mucha confianza en la operatividad de la aplicación. Y lo que es peor, todos los mokas tienen un "precio" oculto que tienes que pagar después de que se escriben las pruebas.
▍ Principio No. 3. Las pruebas deberían facilitar la refactorización del código, no complicarlo.
Las pruebas como la que se muestra arriba complican la refactorización. Si encuentro que en muchos lugares del proyecto hay un código duplicado, y después de un tiempo formateo este código como un componente separado, todas las pruebas para los componentes en los que usaré este nuevo componente fallarán. Los componentes derivados de la técnica de renderizado superficial ya son otra cosa. Donde solía tener marcado repetido, ahora hay un nuevo componente.
Una refactorización más compleja, que implica agregar algunos componentes a un proyecto y eliminar algunos componentes más, genera aún más confusión. El hecho es que debe agregar nuevas pruebas al sistema y eliminar las pruebas innecesarias del mismo. La regeneración de instantáneas es una tarea simple, pero ¿cuál es el valor de tales pruebas? Incluso si pueden encontrar un error, sería mejor si se lo perdieran en una serie de cambios de instantáneas y simplemente verificaran nuevas instantáneas sin dedicar demasiado tiempo a ello.
Como resultado, tales pruebas no ayudan particularmente a refactorizar. Idealmente, ninguna prueba debería fallar si realizo una refactorización, después de lo cual lo que ve el usuario y con lo que interactúa no ha cambiado. Y viceversa: si cambié lo que está contactando el usuario, al menos una prueba debería fallar. Si las pruebas siguen estas dos reglas, son una herramienta excelente para garantizar que algo que los usuarios encuentran no cambie durante la refactorización.
▍ Principio No. 4. Las pruebas deben reproducir cómo los usuarios reales trabajan con la aplicación.
Me gustaría que las pruebas fallaran solo si algo ha cambiado con lo que el usuario interactúa. Esto significa que las pruebas deberían funcionar con la aplicación de la misma manera que los usuarios trabajan con ella. Por ejemplo, una prueba realmente debe interactuar con elementos de formulario y, al igual que un usuario, debe ingresar texto en los campos de entrada de texto. Las pruebas no deberían acceder a los componentes y llamar independientemente a los métodos de su ciclo de vida, no deberían escribir algo en el estado de los componentes o hacer algo que se base en las complejidades de la implementación de los componentes. Dado que, en última instancia, quiero verificar la parte del sistema que está en contacto con el usuario, es lógico esforzarme por asegurar que las pruebas, al interactuar con el sistema, reproduzcan las acciones de los usuarios reales lo más cerca posible.
Herramientas de prueba
Ahora que hemos definido los objetivos que queremos lograr, hablemos sobre las herramientas que hemos elegido para esto.
▍TypeScript
Nuestra base de código usa TypeScript. Nuestros servicios de back-end están escritos en Go e interactúan entre sí mediante gRPC. Esto nos permite generar clientes gRPC escritos para usar en un servidor GraphQL. Los resolvers del servidor GraphQL se escriben usando tipos generados usando
graphql-code-generator . Y, por último, nuestras consultas, mutaciones, así como los componentes y ganchos de suscripción están totalmente escritos. La cobertura completa de nuestra base de código con tipos elimina toda una clase de errores causados por el hecho de que el formulario de datos no es lo que el programador espera. La generación de tipos a partir de los archivos de esquema y protobuf garantiza que todo nuestro sistema, en todas las partes de la pila de tecnologías utilizadas, permanezca homogéneo.
▍Jest (prueba unitaria)
Como marco para probar el código, usamos
Jest y
@ testing-library / react . En las pruebas creadas con estas herramientas, probamos funciones o componentes de forma aislada del resto del sistema. Por lo general, probamos funciones y componentes que se usan con mayor frecuencia en una aplicación, o aquellos que tienen muchas formas de ejecutar código. Tales rutas son difíciles de verificar durante la integración o las pruebas de extremo a extremo (E2E).
Las pruebas unitarias para nosotros son un medio para probar piezas pequeñas. Las pruebas integrales y de extremo a extremo hacen un excelente trabajo al verificar el sistema a mayor escala, lo que le permite verificar el nivel general del estado de la aplicación. Pero a veces debe asegurarse de que los pequeños detalles funcionen, y escribir pruebas de integración para todos los usos posibles del código es demasiado costoso.
Por ejemplo, debemos verificar que la navegación del teclado funcione en el componente responsable de trabajar con la lista desplegable. Pero al mismo tiempo, no querríamos verificar todas las variantes posibles de dicho comportamiento al probar toda la aplicación. Como resultado, probamos exhaustivamente la navegación de forma aislada, y cuando probamos páginas usando el componente apropiado, solo prestamos atención a verificar las interacciones de nivel superior.
Herramientas de prueba
▍Cypress (pruebas de integración)
Las pruebas de integración creadas con
Cypress son el núcleo de nuestro sistema de pruebas. Cuando comenzamos a crear el portal StackPath, estas fueron las primeras pruebas que escribimos, ya que son muy valiosas con muy poca sobrecarga para su creación. Cypress muestra toda nuestra aplicación en un navegador y ejecuta scripts de prueba. Toda nuestra interfaz funciona exactamente de la misma manera que cuando los usuarios trabajan con ella. Es cierto que la capa de red del sistema se reemplaza por mokami. Cada consulta de red que normalmente llegaría al servidor GraphQL devuelve datos condicionales a la aplicación.
El uso de simulacros para simular la capa de red de una aplicación tiene muchos puntos fuertes:
- Las pruebas son más rápidas. Incluso si el backend del proyecto es extremadamente rápido, el tiempo requerido para devolver las respuestas a las solicitudes realizadas durante todo el conjunto de pruebas puede ser bastante considerable. Y si Moki es responsable del retorno de las respuestas, las respuestas se devuelven instantáneamente.
- Las pruebas se están volviendo más confiables. Una de las dificultades de realizar pruebas completas de un proyecto es que es necesario tener en cuenta el estado variable de la red y los datos del servidor, que pueden cambiar. Si se simula el acceso real a la red utilizando moxas, esta variabilidad desaparece.
- Es fácil reproducir situaciones que requieren la repetición exacta de ciertas condiciones. Por ejemplo, en un sistema real, será difícil hacer que ciertas solicitudes fallen de manera estable. Si necesita verificar la reacción correcta de la aplicación ante solicitudes fallidas, entonces moki le permitirá reproducir fácilmente situaciones de emergencia.
Aunque reemplazar todo el backend con mok parece una tarea desalentadora, todos los datos condicionales se escriben usando los mismos tipos de TypeScript generados que se usan en la aplicación. Es decir, esta información, al menos, en términos de estructura, se garantiza que es equivalente a lo que devolvería un backend normal. Durante la mayoría de las pruebas, soportamos con bastante tranquilidad las desventajas de usar mooks en lugar de llamadas reales al servidor.
Además, los programadores están muy contentos de trabajar con Cypress. Las pruebas se ejecutan en el Cypress Test Runner. Las descripciones de prueba se muestran a la izquierda, y la aplicación de prueba se ejecuta en el elemento
iframe
principal. Después de comenzar la prueba, puede estudiar sus etapas individuales y descubrir cómo se comportó la aplicación en un momento u otro. Dado que la herramienta para ejecutar pruebas se ejecuta en el navegador, puede utilizar las herramientas del navegador del desarrollador para depurar las pruebas.
Al escribir pruebas de front-end, a menudo sucede que lleva mucho tiempo comparar lo que hace la prueba con el estado del DOM en un cierto punto de la prueba. Cypress simplifica enormemente esta tarea, ya que el desarrollador puede ver todo lo que sucede con la aplicación bajo prueba.
Aquí hay un video clip que demuestra esto.
Estas pruebas ilustran perfectamente nuestros principios de prueba. La relación entre su valor y su "precio" nos conviene. Las pruebas reproducen de manera muy similar las acciones del usuario real que interactúa con la aplicación. Y solo la capa de red del proyecto fue reemplazada por mokami.
▍Cypress (prueba de extremo a extremo)
Nuestras pruebas E2E también se escriben usando Cypress, pero en ellas no usamos moki para simular el nivel de red de un proyecto o para simular cualquier otra cosa. Al realizar pruebas, la aplicación accede al servidor GraphQL real, que funciona con instancias reales de servicios de back-end.
Las pruebas de extremo a extremo son extremadamente valiosas para nosotros. El hecho es que son los resultados de tales pruebas los que nos permiten saber si algo funciona como se esperaba o no. No se utilizan simulacros durante tales pruebas, como resultado, la aplicación funciona exactamente de la misma manera que cuando es utilizada por clientes reales. Sin embargo, debe tenerse en cuenta que las pruebas de extremo a extremo son "más caras" que otras. Son más lentos, más difíciles de escribir, dada la posibilidad de fallas a corto plazo durante su implementación. Se requiere más trabajo para garantizar que el sistema permanezca en un estado conocido antes de ejecutar las pruebas.
Por lo general, las pruebas deben ejecutarse en un momento en que el sistema se encuentra en algún estado conocido. Una vez completada la prueba, el sistema cambia a otro estado conocido. En el caso de las pruebas de integración, no es difícil lograr este comportamiento del sistema, ya que las llamadas a la API se reemplazan por mokas y, como resultado, cada prueba se ejecuta en condiciones predeterminadas controladas por el programador. Pero en el caso de las pruebas E2E, ya es más difícil hacerlo, ya que el almacén de datos del servidor contiene información que puede cambiar durante la prueba. Como resultado, el desarrollador necesita encontrar alguna forma de asegurarse de que cuando comience la prueba, el sistema esté en un estado previamente conocido.
Al comienzo de la ejecución de prueba de extremo a extremo, ejecutamos un script que, al realizar llamadas directas a la API, crea una nueva cuenta con pilas, sitios, cargas de trabajo, monitores y similares. Cada sesión de prueba implica el uso de una nueva instancia de dicha cuenta, pero todo lo demás de vez en cuando permanece sin cambios. El script, después de hacer todo lo necesario, forma un archivo que contiene los datos que se utilizan para ejecutar las pruebas (generalmente contiene información sobre identificadores de instancia y dominios). Como resultado, resulta que el script le permite llevar el sistema a un estado previamente conocido antes de ejecutar las pruebas.
Dado que las pruebas de extremo a extremo son "más caras" que otros tipos de pruebas, nosotros, en comparación con las pruebas de integración, escribimos menos pruebas de extremo a extremo. Nos esforzamos por garantizar que las pruebas cubran las características críticas de la aplicación. Por ejemplo, esto es registrar usuarios y su inicio de sesión, crear y configurar un sitio / carga de trabajo, etc. Gracias a las extensas pruebas de integración, sabemos que, en general, nuestra interfaz es funcional. Pero las pruebas de extremo a extremo son necesarias solo para asegurarse de que cuando se conecta la interfaz al backend, no ocurra algo que otras pruebas no puedan detectar.
Contras de nuestra estrategia de prueba integral
Aunque estamos muy satisfechos con las pruebas y la estabilidad de la aplicación, también existen desventajas al usar una estrategia de prueba integral como la nuestra.
Para comenzar, la aplicación de dicha estrategia de prueba significa que todos los miembros del equipo deben estar familiarizados con muchas herramientas de prueba, y no solo con una. Todos necesitan saber Jest, @ testing-library / react y Cypress. Pero al mismo tiempo, los desarrolladores no solo necesitan conocer estas herramientas. También deben poder tomar decisiones sobre en qué situación se debe utilizar. ¿Vale la pena probar alguna nueva oportunidad para escribir una prueba de extremo a extremo, o es suficiente la prueba de integración? ¿Es necesario, además de la prueba de extremo a extremo o de integración, escribir una prueba unitaria para verificar los pequeños detalles de la implementación de esta nueva característica?
Sin lugar a dudas, esto, por así decirlo, "carga la cabeza" de nuestros programadores, mientras usan la única herramienta que no experimentarían tal carga. Por lo general, comenzamos con las pruebas de integración, y después de eso, si vemos que la característica en estudio es de particular importancia y depende en gran medida de la parte del servidor del proyecto, agregamos la prueba de extremo a extremo adecuada. O comenzamos con pruebas unitarias, haciendo esto si creemos que una prueba unitaria no podrá verificar todas las sutilezas de implementar un mecanismo determinado.
Por supuesto, todavía nos enfrentamos a situaciones en las que no está claro por dónde empezar. Pero, como constantemente tenemos que tomar decisiones con respecto a las pruebas, comienzan a surgir ciertos patrones de situaciones comunes. Por ejemplo, generalmente probamos sistemas de validación de formularios mediante pruebas unitarias. Esto se hace debido al hecho de que durante la prueba debe verificar muchos escenarios diferentes. Al mismo tiempo, todos en el equipo lo saben y no pierden el tiempo planeando una estrategia de prueba cuando uno de ellos necesita probar el sistema de validación de formularios.
Otro inconveniente del enfoque que utilizamos es la complicación de recopilar datos sobre la cobertura del código mediante pruebas. Aunque esto es posible, es mucho más complicado que en una situación en la que uno se usa para probar un proyecto. Aunque la búsqueda de un número hermoso de cobertura de código mediante pruebas puede conducir a un deterioro en la calidad de las pruebas, dicha información es valiosa en términos de encontrar "agujeros" en el conjunto de pruebas utilizado. El problema de usar varias herramientas de prueba es que, para comprender qué parte del código no se ha probado, debe combinar informes sobre la cobertura del código con las pruebas recibidas de diferentes sistemas. Es posible, pero definitivamente es mucho más difícil que leer un informe generado por cualquier medio de prueba.
Resumen
Cuando utilizamos muchas herramientas de prueba, nos enfrentamos a tareas difíciles. Pero cada una de estas herramientas cumple su propio propósito. Al final, creemos que hicimos lo correcto al incluirlos en nuestro sistema de prueba de código. Pruebas de integración: aquí es donde es mejor comenzar a crear un sistema de prueba al comienzo del trabajo en una nueva aplicación o al equipar pruebas de un proyecto existente. Será útil tratar de agregar pruebas de extremo a extremo al proyecto lo antes posible, verificando las características más importantes del proyecto.
Cuando hay pruebas integrales y de extremo a extremo en el conjunto de pruebas, esto debería llevar al hecho de que el desarrollador recibirá un cierto nivel de confianza en la operatividad de la aplicación cuando se realicen cambios. Si, durante el curso del trabajo en el proyecto, comenzaron a aparecer errores que no son detectados por las pruebas, vale la pena considerar qué pruebas podrían detectar estos errores y si la aparición de errores indica fallas en todo el sistema de prueba utilizado en el proyecto.
Por supuesto, no acudimos de inmediato a nuestro sistema de prueba actual. Además, esperamos que este sistema, a medida que nuestro proyecto crezca, se desarrolle. Pero ahora realmente nos gusta nuestro enfoque para las pruebas.
Estimados lectores! ¿Qué estrategias sigues en las pruebas frontend? ¿Qué herramientas de prueba frontend usas?
