Introducción a las pruebas
Publicado: 2021-06-16Bienvenido a nuestra nueva serie de blogs sobre todo lo relacionado con las pruebas . Con suerte, estas publicaciones de blog le darán una idea general de cómo las escribimos aquí en Mediatoolkit y por qué. Antes de sumergirnos en los ejemplos, comencemos con algunas definiciones básicas, ideas y beneficios de escribir pruebas.
¿Qué tipo de pruebas hay?
No todas las pruebas son iguales. Hay diferentes tipos de pruebas para diferentes propósitos. Si bien esta publicación se enfoca principalmente en las pruebas unitarias , debemos ser conscientes de las diferencias y comprender los beneficios de cada una.
1. Pruebas manuales
Las pruebas manuales implican pruebas ejecutadas manualmente por un probador. Estos generalmente los realiza Quality Assurance o los propios desarrolladores . El control de calidad debe verse como la última línea de defensa, no como el probador principal de la aplicación (el control de calidad que realiza pruebas manuales como la forma principal de probar la interfaz de usuario es una excepción válida en este caso).
Los desarrolladores generalmente escriben pruebas manuales cuando quieren ejecutar algunas cosas localmente y ver cómo se comporta el sistema sin enviar ningún código a la fase de preparación, o Dios te salve, producción. Sin embargo, el objetivo de estas pruebas no es la solidez del código. La única función de estas pruebas es detectar errores y garantizar la calidad de su producto .
2. Pruebas de integración
Los sistemas están compuestos de múltiples componentes, o al menos deberían estarlo. Las pruebas de integración verifican la API pública de estos componentes. No solo la API REST, sino cualquier API expuesta públicamente, como su comunicación de tema de Kafka.
Si un componente "dice" que emite un mensaje sobre un tema en un formato determinado, esa es su API pública o contrato. Las pruebas de integración verifican si varios componentes desarrollados individualmente pueden comunicarse y si sus contratos coinciden como grupo.
Si solo prueba estos componentes individualmente, su comportamiento individual podría funcionar correctamente, pero cuando intenta conectarlos en un grupo más grande, descubre que sus contratos difieren .
3. Pruebas de extremo a extremo (E2E)
Las pruebas de extremo a extremo garantizan el flujo de la experiencia del usuario del producto final . La complejidad de los sistemas actuales es difícil de cubrir con pruebas. Muchos sistemas dependen unos de otros y las pruebas E2E aseguran que el producto hace lo que se espera . El control de calidad valida la corrección de su producto al pasar por el flujo de usuarios finales y verificar si todos los sistemas se comportan como se espera.
4. Pruebas unitarias
Las pruebas unitarias son la columna vertebral de cualquier software confiable . Hacen la base para otras pruebas. Prueban unidades individuales.
Los desarrolladores pueden confundir la definición de una unidad con un solo método. Las pruebas unitarias deben probar el comportamiento de un componente que podría estar compuesto por múltiples clases diferentes. Los métodos públicos accesibles por otros componentes deben probarse, no pruebe métodos o clases protegidos. Son detalles de implementación, no forman parte de su API pública .
¿Por qué deberíamos siquiera probar?
Si escribimos buenas pruebas y las escribimos con frecuencia, podemos garantizar la calidad de nuestro producto antes de que vea la luz del día.
A medida que nuestro sistema envejece, los beneficios de las pruebas se vuelven más y más evidentes. Nuestros entornos se vuelven confiables, ahorran tiempo de desarrollo y muchos tirones de cabello cuando inevitablemente las cosas salen mal. Sus colegas estarán agradecidos cuando puedan echar un vistazo a sus pruebas y comprender lo que su código debe y no debe hacer sin tener que ejecutar las cosas manualmente .
Código robusto
Antes de empezar a hablar de “código robusto”, ¿cómo deberíamos definirlo? ¿Qué hace que el código sea robusto? ¿Significa eso que debemos programar a la defensiva y pensar en cómo otros desarrolladores podrían abusar de nuestro código?
El código robusto siempre es simple y limpio. La simplicidad es robusta, la complejidad es frágil . Deberíamos manejar entradas no válidas, pero eso no significa que debamos programar a la defensiva y no confiar en nuestro equipo.
Ley de Gall : Invariablemente se encuentra que un sistema complejo que funciona ha evolucionado a partir de un sistema simple que funcionó.
La proposición inversa también parece ser cierta: un sistema complejo diseñado desde cero nunca funciona y no se puede hacer que funcione. Tienes que empezar de nuevo, comenzando con un sistema simple que funcione.
Refactorizar de forma segura
Cuando su código está cubierto por pruebas, ya no tiene miedo de cambiar el código existente . Después de cada cambio, puede ejecutar sus pruebas y asegurarse de no romper nada. Cuando tienes pruebas, no tienes que programar a la defensiva.
Refactorizar sin pruebas es ir por una pendiente resbaladiza que terminará en noches de insomnio y domingos de trabajo. Este tema es demasiado amplio para tratarlo aquí y merece una publicación de blog propia en el futuro.
¿Cómo no escribimos pruebas?
Es tan importante saber cómo NO escribir pruebas unitarias como cómo escribirlas.
- Escribir una prueba que imprima el resultado de una llamada a un método no es una prueba ya que no validamos el resultado deseado .
- Si su prueba lee datos de un archivo en su carpeta Documentos, no es una prueba de unidad real ya que las pruebas no deberían depender del entorno.
- Cualquier desarrollador debería poder verificar su código y ejecutar las pruebas con éxito sin hacer nada más.
- Cada prueba unitaria debe ser independiente de otras pruebas . Eso implica que el orden de ejecución de sus pruebas tampoco debería importar.
- Ejecutar pruebas varias veces siempre debería terminar con los mismos resultados si no cambiamos ningún código.
- Las pruebas unitarias deben probar comportamientos , no llamadas a métodos individuales. No todas las clases y métodos necesitan tener su prueba.
El comportamiento es algo que produce un valor real que el usuario de su sistema necesita. ¿Su usuario necesita saber si productFactory.create() creó el mismo objeto cuando se llamó dos veces o si se llamó a su repositorio con algunos parámetros? Probablemente no, pero aún muchos desarrolladores escriben exactamente este tipo de pruebas.
Si sus pruebas se ven así, están estrechamente relacionadas con su implementación. Cada vez que desee cambiar los detalles de su implementación, debe actualizar sus pruebas, aunque el comportamiento sea el mismo. Sus pruebas deben cambiar solo cuando cambia el comportamiento, no los detalles de implementación . En otras palabras, prueba lo que hace tu código, no cómo lo hace.
¿Cómo escribimos las pruebas?
Nuestras pruebas deben seguir las mejores prácticas de código , deben ser independientes del entorno y deben ejecutarse rápidamente .
Es importante mantener el tiempo de ejecución de la prueba lo más corto posible. Cada prueba no debería tomar más de un par de milisegundos . Cuando las pruebas tardan demasiado en ejecutarse, las personas tienden a omitirlas y solo confían en su servidor de CI, como Jenkins, para armar un escándalo cuando no puede crear sus ejecutables de implementación.
Cada prueba se compone de 3 secciones 'A' (el patrón AAA) :

- Arreglar
- Actuar
- Afirmar
1. Organizar
En la sección de organización de nuestra prueba, nos aseguramos de que nuestro sistema esté en un estado específico antes de llamar al comportamiento que queremos probar . El 'sistema' podría ser un objeto que necesitamos configurar de una manera específica para producir un comportamiento, creando archivos temporales o cosas de esa naturaleza.
El código de esta sección suele ser más grande que los otros dos combinados .
Un patrón de diseño que debería resultar particularmente útil para mantener pequeña esta sección es Object Mother . Este patrón de diseño es muy similar a Factory , pero tiene métodos más específicos que crean objetos preconfigurados para usted. Mientras que una Factory estándar podría tener un método como createCar(carDescription) , ObjectMother tendrá métodos como createRedFerrari() , createBlackTesla() o createBrokenYugo() .
2. Actuar
Esta sección de su prueba debe tener una línea . Esta línea ejecuta el comportamiento bajo prueba. Si se encuentra escribiendo más de una línea para esta sección, probablemente no tenga la encapsulación correcta de su comportamiento . No se debe esperar que sus clientes llamen a múltiples métodos de su objeto en un orden particular, entonces, ¿por qué lo harían sus pruebas?
Esta línea es una llamada de método que queremos probar. Si este método devuelve un resultado, debe almacenar ese valor en una variable para verificar si es el valor esperado en el paso Afirmar.
3. Afirmar
Después de que hayamos preparado el sistema en la sección Organizar y ejecutado nuestra acción bajo prueba en la sección Actuar, necesitamos validar el resultado de la acción. Por lo general, verificamos el resultado del método aquí, pero a veces, nuestros métodos no devuelven valores, pero aún producen efectos secundarios. Si se esperaba que nuestro código cambiara el estado de un objeto, creara un archivo o eliminara algo de una List , deberíamos verificar si hizo exactamente eso.
Talones vs simulacros
La mayoría de los desarrolladores usan los términos simulacro y código auxiliar indistintamente, pero existen diferencias.
Un talón no puede fallar en la prueba, un simulacro sí.
Los stubs se basan en el estado , devuelven valores codificados. “¿Qué resultado obtuve?”
Los simulacros se basan en el comportamiento , los usa para verificar cómo pasa su comportamiento a través de él. “¿Cómo obtuve el resultado?”
Si sus pruebas unitarias están plagadas de simulacros, sus pruebas terminan siendo muy frágiles, lo que significa que cada vez que cambia uno de sus detalles de implementación, debe actualizar todas sus llamadas simuladas.
Prueba solo una cosa.
Necesitamos poder aislar un comportamiento y demostrar que funciona . Si ese comportamiento debería funcionar de manera diferente con diferentes entradas, necesitamos escribir una nueva prueba para cada uno de esos comportamientos. Es difícil saber por qué nuestra prueba falló si tenemos una prueba grande que prueba varias cosas a la vez. Además, se vuelve más difícil eliminar funciones que ya no necesitamos y ver qué funciones rompimos cuando agregamos código nuevo.
Está perfectamente bien tener múltiples aserciones en la sección final de sus pruebas, siempre y cuando eso no requiera que llame al comportamiento bajo prueba varias veces. Si ese es el caso, será difícil identificar el comportamiento defectuoso y solucionarlo.
Cuando afirmamos múltiples comportamientos en una prueba, no obtenemos una imagen clara de lo que no funciona exactamente porque la prueba informará solo la primera falla y el resto se omitirá. Se vuelve mucho más difícil entender qué cambios son necesarios y cuántas cosas no funcionan como se esperaba.
Pruebas de nombres
Una cosa que separa las buenas pruebas de las grandes pruebas es el nombre de la prueba. Las pruebas no solo deben decirnos qué hacen, sino cuándo lo hacen.
Hay muchos buenos patrones de nomenclatura que puede usar, así que elija el que le resulte más descriptivo y quédese con él. Estos son algunos ejemplos de excelentes nombres de prueba:
- El servicio de registro debe. crear una nueva cuenta cuando no se toma el correo electrónico
- RegistrationServiceTest.whenEmailIsFree_createNewAccount
- RegistrationServiceTest.if_freeEmail_when_userCreatesAccount_then_create
Cuando escribe pruebas unitarias, es mucho más importante comunicar lo que está probando que seguir las mejores prácticas de nomenclatura de métodos. Por ejemplo, en Java, usamos camelCase cuando escribimos métodos, pero es perfectamente válido usar un guión bajo (_) para separar el estado de la acción en su nombre de prueba.
Pruebas limpias
Las pruebas que escriba deben seguir todas las prácticas de código limpio que aplica a su código. Las pruebas no son ciudadanos de segunda clase y debe aplicar el mismo nivel de atención que con el resto de su código para que sean legibles.
La definición de duplicación de código en las pruebas es muy importante. El principio DRY (Don't Repeat Yourself) se aplica a la extracción de comportamientos que cambian por la misma razón. Las pruebas cambian por diferentes razones, por lo tanto, varíe al extraer cosas de sus pruebas si realmente no cambian por la misma razón. Alerta de spoiler, a menudo no lo hacen.
if las declaraciones no pertenecen a las pruebas. La declaración if nos dice que nuestra prueba hace al menos dos cosas diferentes y que estaríamos mejor si reescribiéramos nuestra prueba como dos pruebas diferentes. Cuando se nombra correctamente, será más fácil comprender qué hacen las pruebas y cuáles son todos los diferentes comportamientos.
¿Cuándo debemos escribir pruebas?
Guiados por los principios de TDD, debemos escribir pruebas antes de escribir código nuevo .
Cuando necesitamos agregar una nueva característica, primero describimos el comportamiento deseado como una nueva prueba. Hacemos la menor cantidad de cambios necesarios para pasar esa prueba sin romper ninguna otra.
De lo contrario, cuanto más tiempo pasa, más código tenemos que no ha sido probado y aumenta la posibilidad de introducir errores o sobreingeniería .
Además, las pruebas se vuelven más complejas ya que ahora tenemos más código para probar, y lo que suele suceder es que los desarrolladores ajustan las pruebas al código. Eso significa que ajustamos el comportamiento para que coincida con lo que hace nuestro código y no al revés.
Cuando escribimos pruebas temprano, la definición del problema se vuelve más pequeña y es más fácil entender tales problemas que comportamientos más genéricos y complejos.
Pensamientos finales
Si bien escribir pruebas puede parecer algo opcional, es crucial comenzar con una buena base. La codificación ya es difícil, hágalo más fácil para usted y sus compañeros de equipo escribiendo un código comprobable que sea más fácil de leer, comprender y mantener . Finalmente, si tiene dificultades para hacer que sus pruebas cumplan con sus deseos, es más probable que el problema esté en su código que en sus pruebas.
¿Está interesado en trabajar en un código limpio y comprobable?
Echa un vistazo a nuestra posición abierta para Senior Backend Developer
o envíenos una solicitud abierta !
