Según tengo entendido, el objetivo de las pruebas unitarias es probar las unidades de código de forma aislada . Esto significa que:
- No deben interrumpir ningún cambio de código no relacionado en otra parte de la base de código.
- Solo una prueba de unidad debe romperse por un error en la unidad probada, en oposición a las pruebas de integración (que pueden romperse en montones).
Todo esto implica que todas las dependencias externas de una unidad probada deben ser burladas. Y me refiero a todas las dependencias externas , no solo a las "capas externas" como redes, sistema de archivos, base de datos, etc.
Esto lleva a una conclusión lógica, que prácticamente todas las pruebas unitarias deben burlarse . Por otro lado, una búsqueda rápida en Google sobre burla revela toneladas de artículos que afirman que "burlarse es un olor a código" y que debe evitarse en su mayoría (aunque no completamente).
Ahora, a la (s) pregunta (s).
- ¿Cómo deben escribirse correctamente las pruebas unitarias?
- ¿Dónde se encuentra exactamente la línea entre ellos y las pruebas de integración?
Actualización 1
Considere el siguiente pseudocódigo:
class Person {
constructor(calculator) {}
calculate(a, b) {
const sum = this.calculator.add(a, b);
// do some other stuff with the `sum`
}
}
¿Se puede considerar una prueba unitaria una prueba que prueba el Person.calculate
método sin burlarse de la Calculator
dependencia (dado que Calculator
es una clase ligera que no accede al "mundo exterior")?
fuente
Respuestas:
Martin Fowler en prueba de unidad
Lo que Kent Beck escribió en Test Driven Development, por ejemplo
Cualquier afirmación dada de "el punto de las pruebas unitarias es" dependerá en gran medida de qué definición de "prueba unitaria" se está considerando.
Si su perspectiva es que su programa se compone de muchas unidades pequeñas que dependen unas de otras, y si se limita a un estilo que pruebe cada unidad de forma aislada, muchas pruebas de duplicación son una conclusión inevitable.
El consejo contradictorio que ve proviene de personas que operan bajo un conjunto diferente de supuestos.
Por ejemplo, si está escribiendo pruebas para apoyar a los desarrolladores durante el proceso de refactorización, y dividir una unidad en dos es una refactorización que debe ser compatible, entonces algo debe darse. ¿Quizás este tipo de prueba necesita un nombre diferente? O tal vez necesitamos una comprensión diferente de "unidad".
Es posible que desee comparar:
Creo que esa es la pregunta incorrecta que hacer; nuevamente es un argumento sobre las etiquetas , cuando creo que lo que realmente nos importa son las propiedades .
Cuando introduzco cambios en el código, no me importa el aislamiento de las pruebas: ya sé que "el error" está en algún lugar de mi pila actual de ediciones no verificadas. Si ejecuto las pruebas con frecuencia, entonces limito la profundidad de esa pila, y encontrar el error es trivial (en el caso extremo, las pruebas se ejecutan después de cada edición; la profundidad máxima de la pila es una). Pero ejecutar las pruebas no es el objetivo, es una interrupción, por lo que tiene valor reducir el impacto de la interrupción. Una forma de reducir la interrupción es asegurarse de que las pruebas sean rápidas ( Gary Bernhardt sugiere 300 ms , pero no he descubierto cómo hacerlo en mis circunstancias).
Si invocar
Calculator::add
no aumenta significativamente el tiempo requerido para ejecutar la prueba (o cualquiera de las otras propiedades importantes para este caso de uso), entonces no me molestaría en usar una prueba doble: no proporciona beneficios que superen los costos .Observe los dos supuestos aquí: un ser humano como parte de la evaluación de costos, y la pequeña pila de cambios no verificados en la evaluación de beneficios. En circunstancias donde esas condiciones no se cumplen, el valor de "aislamiento" cambia bastante.
Ver también la lava caliente , por Harry Percival.
fuente
Al minimizar los efectos secundarios en su código.
Tomando su código de ejemplo, si,
calculator
por ejemplo, habla con una API web, entonces crea pruebas frágiles que dependen de poder interactuar con esa API web, o crea una simulación de ella. Sin embargo, si se trata de un conjunto de funciones de cálculo determinista y sin estado, entonces no (y no debería) burlarse de él. Si lo hace, corre el riesgo de que su simulacro se comporte de manera diferente al código real, lo que provocará errores en sus pruebas.Los simulacros solo deberían ser necesarios para el código que lee / escribe en el sistema de archivos, bases de datos, puntos finales de URL, etc. que dependen del entorno en el que se está ejecutando; o que son de naturaleza altamente declarativa y no determinista. Entonces, si mantiene esas partes del código al mínimo y las oculta detrás de abstracciones, entonces son fáciles de burlarse y el resto de su código evita la necesidad de simulacros.
Para los puntos de código que tienen efectos secundarios, vale la pena escribir pruebas que se burlen y pruebas que no. Sin embargo, estos últimos necesitan cuidados, ya que serán inherentemente frágiles y posiblemente lentos. Por lo tanto, es posible que solo desee ejecutarlos durante la noche en un servidor de CI, en lugar de cada vez que guarde y cree su código. Sin embargo, las pruebas anteriores deben ejecutarse con la mayor frecuencia posible. En cuanto a si cada prueba es entonces una prueba de unidad o integración, se convierte en académica y evita las "guerras de llamas" sobre lo que es y no es una prueba de unidad.
fuente
R.equals
). Debido a que estas son en su mayoría funciones puras, generalmente no se burlan en las pruebas.Estas preguntas son bastante diferentes en su dificultad. Tomemos la pregunta 2 primero.
Las pruebas unitarias y las pruebas de integración están claramente separadas. Una prueba de unidad prueba una unidad (método o clase) y usa otras unidades solo la cantidad necesaria para lograr ese objetivo. Puede ser necesario burlarse, pero no es el objetivo de la prueba. Una prueba de integración prueba la interacción entre diferentes unidades reales . Esta diferencia es la razón por la que necesitamos pruebas unitarias y de integración: si una hiciera el trabajo de la otra lo suficientemente bien, no lo haríamos, pero resultó que generalmente es más eficiente usar dos herramientas especializadas en lugar de una herramienta generalizada .
Ahora para la pregunta importante: ¿Cómo se debe probar la unidad? Como se dijo anteriormente, las pruebas unitarias deben construir estructuras auxiliares solo en la medida necesaria . A menudo es más fácil usar una base de datos simulada que su base de datos real o incluso cualquier base de datos real. Sin embargo, burlarse en sí mismo no tiene valor. Si a menudo sucede que, de hecho, es más fácil usar componentes reales de otra capa como entrada para una prueba de unidad de nivel medio. Si es así, no dudes en usarlos.
Muchos profesionales temen que si la prueba unitaria B reutiliza las clases que ya fueron probadas por la prueba unitaria A, entonces un defecto en la unidad A causa fallas en la prueba en varios lugares. Considero que esto no es un problema: tiene un conjunto de pruebas para tener éxito 100% con el fin de darle la tranquilidad que necesita, por lo que no es un gran problema a tener demasiados fracasos - después de todo, hacer tener un defecto. El único problema crítico sería si un defecto provocara muy pocas fallas.
Por lo tanto, no hagas una religión de burla. Es un medio, no un fin, por lo que si puede evitar el esfuerzo adicional, debe hacerlo.
fuente
The only critical problem would be if a defect triggered too few failures.
Este es uno de los puntos débiles de la burla. Tenemos que "programar" el comportamiento esperado, por lo que podríamos fallar al hacerlo, haciendo que nuestras pruebas terminen como "falsos positivos". Pero burlarse es una técnica muy útil para lograr el determinismo (la condición más importante de las pruebas). Los uso en todos mis proyectos cuando es posible. También me muestran cuando la integración es demasiado compleja o la dependencia demasiado estrecha.OK, para responder sus preguntas directamente:
Como dices, deberías burlarte de las dependencias y probar solo la unidad en cuestión.
Una prueba de integración es una prueba unitaria en la que no se burlan de sus dependencias.
No. Debe inyectar la dependencia de la calculadora en este código y puede elegir entre una versión simulada o una real. Si usa uno simulado, es una prueba unitaria, si usa uno real, es una prueba de integración.
Sin embargo, una advertencia. ¿realmente te importa cómo la gente cree que deberían llamarse tus pruebas?
Pero su verdadera pregunta parece ser esta:
Creo que el problema aquí es que mucha gente usa simulacros para recrear completamente las dependencias. Por ejemplo, podría burlarme de la calculadora en tu ejemplo como
No haría algo como:
Yo diría que eso sería "probar mi simulacro" o "probar la implementación". Yo diría " ¡No escribas simulacros! * Así".
Otras personas no estarían de acuerdo conmigo, comenzaríamos guerras masivas de llamas en nuestros blogs sobre la Mejor manera de burlarse, lo que realmente no tendría sentido a menos que entendiera todo el trasfondo de los diversos enfoques y realmente no ofrezca mucho valor a alguien que solo quiere escribir buenas pruebas.
fuente
MockCalc
aStubCalc
, y lo llamaría un trozo, no un simulacro. martinfowler.com/articles/…Mi regla general es que las pruebas unitarias adecuadas:
Si hay clases de utilidad de marcos que complican las pruebas unitarias, incluso puede resultarle útil crear interfaces y clases "envolventes" muy simples para facilitar la burla de esas dependencias. Esas envolturas no estarían necesariamente sujetas a pruebas unitarias.
He encontrado esta distinción como la más útil:
Hay área gris aquí. Por ejemplo, si puede ejecutar una aplicación en un contenedor Docker y ejecutar las pruebas de integración como la etapa final de una compilación, y destruir el contenedor después, ¿está bien incluir esas pruebas como "pruebas unitarias"? Si este es tu debate candente, estás en un lugar bastante bueno.
No. Algunos casos de prueba individuales serán para condiciones de error, como pasar
null
como parámetro y verificar que obtenga una excepción. Muchas pruebas como esa no requerirán ningún simulacro. Además, las implementaciones que no tienen efectos secundarios, por ejemplo, el procesamiento de cadenas o las funciones matemáticas, pueden no requerir simulacros porque simplemente verifica la salida. Pero creo que la mayoría de las clases que valen la pena requerirán al menos un simulacro en alguna parte del código de prueba. (Cuanto menos, mejor).El problema de "olor a código" que mencionó surge cuando tiene una clase que es demasiado complicada, que requiere una larga lista de dependencias simuladas para poder escribir sus pruebas. Esta es una pista de que necesita refactorizar la implementación y dividir las cosas, para que cada clase tenga una huella más pequeña y una responsabilidad más clara y, por lo tanto, sea más fácilmente comprobable. Esto mejorará la calidad a largo plazo.
No creo que sea una expectativa razonable, porque funciona contra la reutilización. Es posible que tenga un
private
método, por ejemplo, llamado por variospublic
métodos publicados por su interfaz. Un error introducido en ese método podría causar múltiples fallas de prueba. Esto no significa que deba copiar el mismo código en cadapublic
método.fuente
No estoy realmente seguro de cómo es útil esta regla. Si un cambio en una clase / método / lo que sea puede romper el comportamiento de otra en el código de producción, entonces las cosas son, en realidad, colaboradoras y no están relacionadas. Si sus pruebas se rompen y su código de producción no, entonces sus pruebas son sospechosas.
Consideraría esta regla con sospecha también. Si eres lo suficientemente bueno como para estructurar tu código y escribir tus pruebas de manera que un error cause exactamente una falla en la prueba de la unidad, entonces estás diciendo que ya has identificado todos los errores potenciales, incluso a medida que la base de código evoluciona para usar casos No lo he anticipado.
No creo que sea una distinción importante. ¿Qué es una 'unidad' de código de todos modos?
Intente encontrar puntos de entrada en los que pueda escribir pruebas que simplemente 'tengan sentido' en términos de las reglas de dominio / negocio problemáticas con las que se enfrenta ese nivel del código. A menudo, estas pruebas son de naturaleza algo "funcional": ingrese una entrada y compruebe que la salida es la esperada. Si las pruebas expresan un comportamiento deseado del sistema, a menudo permanecen bastante estables incluso a medida que el código de producción evoluciona y se refactoriza.
No lea demasiado la palabra 'unidad', e inclínese hacia el uso de sus clases de producción reales en las pruebas, sin preocuparse demasiado si está involucrando a más de uno de ellos en una prueba. Si uno de ellos es difícil de usar (porque requiere mucha inicialización o necesita golpear una base de datos / servidor de correo electrónico real, etc.), deje que sus pensamientos se vuelvan burlones / falsos.
fuente
Person:tellStory()
método que incorpora los detalles de una persona en una cadena y luego lo devuelve, entonces la "historia" es probablemente una unidad. Si creo un método auxiliar privado que oculta parte del código, entonces no creo que haya introducido una nueva unidad, no necesito probarlo por separado.Primero, algunas definiciones:
Una prueba de unidad prueba las unidades de forma aislada de otras unidades, pero lo que eso significa no está definido concretamente por ninguna fuente autorizada, así que definámoslo un poco mejor: si se cruzan los límites de E / S (si esa E / S es red, disco, pantalla o entrada UI), hay un lugar semi-objetivo donde podemos dibujar una línea. Si el código depende de E / S, está cruzando el límite de una unidad y, por lo tanto, tendrá que burlarse de la unidad responsable de esa E / S.
Según esa definición, no veo una razón convincente para burlarse de cosas como funciones puras, lo que significa que las pruebas unitarias se prestan a funciones puras o funciones sin efectos secundarios.
Si desea unir las unidades de prueba con efectos, las unidades responsables de los efectos deben ser burladas, pero quizás debería considerar una prueba de integración. Entonces, la respuesta corta es: "si necesita burlarse, pregúntese si lo que realmente necesita es una prueba de integración". Pero hay una respuesta mejor y más larga aquí, y la madriguera del conejo es mucho más profunda. Los simulacros pueden ser el olor de mi código favorito porque hay mucho que aprender de ellos.
Olores de código
Para esto, pasaremos a Wikipedia:
Continúa más tarde ...
En otras palabras, no todos los olores de código son malos. En cambio, son indicaciones comunes de que algo podría no expresarse en su forma óptima, y el olor puede indicar una oportunidad para mejorar el código en cuestión.
En el caso de burlarse, el olor indica que las unidades que parecen estar pidiendo burlas dependen de las unidades que se burlan. Puede ser una indicación de que no hemos descompuesto el problema en piezas con solución atómica, y eso podría indicar una falla de diseño en el software.
La esencia de todo desarrollo de software es el proceso de dividir un gran problema en partes más pequeñas e independientes (descomposición) y componer las soluciones juntas para formar una aplicación que resuelva el gran problema (composición).
Se requiere burlarse cuando las unidades utilizadas para dividir el gran problema en partes más pequeñas dependen unas de otras. Dicho de otra manera, se requiere burlarse cuando nuestras supuestas unidades de composición atómica no son realmente atómicas, y nuestra estrategia de descomposición no ha logrado descomponer el problema más grande en problemas más pequeños e independientes para ser resueltos.
Lo que hace que burlarse de un código huela no es que haya algo intrínsecamente malo con la burla, a veces es muy útil. Lo que lo convierte en un olor a código es que podría indicar una fuente problemática de acoplamiento en su aplicación. A veces, eliminar esa fuente de acoplamiento es mucho más productivo que escribir un simulacro.
Hay muchos tipos de acoplamiento, y algunos son mejores que otros. Comprender que los simulacros son un olor a código puede enseñarle a identificar y evitar los peores tipos al principio del ciclo de vida del diseño de la aplicación, antes de que el olor se convierta en algo peor.
fuente
La burla solo debe usarse como último recurso, incluso en pruebas unitarias.
Un método no es una unidad, e incluso una clase no es una unidad. Una unidad es cualquier separación lógica de código que tenga sentido, independientemente de cómo se llame. Un elemento importante de tener un código bien probado es poder refactorizar libremente, y parte de poder refactorizar libremente significa que no tiene que cambiar sus pruebas para hacerlo. Cuanto más te burles, más tendrás que cambiar tus pruebas cuando refactorices. Si considera el método como la unidad, debe cambiar sus pruebas cada vez que refactorice. Y si considera que la clase es la unidad, entonces debe cambiar sus pruebas cada vez que quiera dividir una clase en varias clases. Cuando tiene que refactorizar sus pruebas para refactorizar su código, hace que las personas elijan no refactorizar su código, que es lo peor que le puede pasar a un proyecto. Es esencial que pueda dividir una clase en varias clases sin tener que refactorizar sus pruebas, o terminará con clases de espagueti de 500 líneas de gran tamaño. Si está tratando métodos o clases como sus unidades con pruebas unitarias, probablemente no esté haciendo Programación Orientada a Objetos, sino algún tipo de programación funcional mutante con objetos.
Aislar tu código para una prueba unitaria no significa que te burles de todo lo que está fuera de él. Si lo hiciera, tendría que burlarse de la clase de matemáticas de su idioma, y absolutamente nadie piensa que sea una buena idea. Las dependencias internas no deben tratarse de manera diferente a las dependencias externas. Confía en que están bien probados y funcionan como se supone que deben hacerlo. La única diferencia real es que si sus dependencias internas están rompiendo sus módulos, puede detener lo que está haciendo para solucionarlo en lugar de tener que publicar un problema en GitHub y cavar en una base de código que no entiende para solucionarlo. o espero lo mejor.
Aislar su código solo significa que trata sus dependencias internas como cuadros negros y no prueba las cosas que están sucediendo dentro de ellos. Si tiene el Módulo B que acepta entradas de 1, 2 o 3, y tiene el Módulo A, que lo llama, no tiene sus pruebas para que el Módulo A haga cada una de esas opciones, simplemente elija una y úsela. Significa que sus pruebas para el Módulo A deben evaluar las diferentes formas en que trata las respuestas del Módulo B, no las cosas que le pasa.
Entonces, si su controlador pasa un objeto complejo a una dependencia, y esa dependencia hace varias cosas posibles, tal vez guardándola en la base de datos y tal vez devolviendo una variedad de errores, pero todo lo que su controlador realmente hace es simplemente verificar para ver si regresa un error o no y pasa esa información, entonces todo lo que prueba en su controlador es una prueba de si devuelve un error y lo pasa y una prueba de si no devuelve un error. No prueba si algo se guardó en la base de datos o qué tipo de error es el error, porque eso sería una prueba de integración. No tiene que burlarse de la dependencia para hacer esto. Has aislado el código.
fuente