¿Cómo deben escribirse exactamente las pruebas unitarias sin burlarse ampliamente?

64

Según tengo entendido, el punto de las pruebas unitarias es probar unidades de código aisladas . Esto significa que:

  1. No deben interrumpir por ningún código no relacionado en otra parte del código base.
  2. Solo una prueba de unidad debe interrumpirse por un error en la unidad probada, a diferencia de 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, sistemas de archivos, bases de datos, etc.

Esto lleva a una conclusión lógica, que prácticamente todas las pruebas de unidad deben simularse . Por otro lado, una rápida búsqueda en Google sobre la burla revela toneladas de artículos que afirman que "la burla es un olor a código" y que, en su mayoría, debe evitarse (aunque no completamente).

Ahora, a la (s) pregunta (s).

  1. ¿Cómo deben escribirse correctamente las pruebas unitarias?
  2. ¿Dónde se encuentra exactamente la línea entre ellos y las pruebas de integración?

Actualización 1

Considere el siguiente pseudo código:

class Person {
    constructor(calculator) {}

    calculate(a, b) {
        const sum = this.calculator.add(a, b);

        // do some other stuff with the 'sum'
    }
}

¿Una prueba que prueba el método Person.calculate sin burlarse de la dependencia Calculator (dado que Calculator es una clase liviana que no accede al "mundo exterior") puede considerarse una prueba unitaria?

    
pregunta Alexander Lomia 27.11.2018 - 11:46

6 respuestas

52
  

el punto de pruebas unitarias es probar unidades de código aisladas.

Martin Fowler en Prueba de unidad

  

La prueba de unidad a menudo se habla en el desarrollo de software, y es un término con el que he estado familiarizado durante todo el tiempo que escribo programas. Sin embargo, como la mayoría de la terminología de desarrollo de software, está muy mal definida, y veo que la confusión a menudo puede ocurrir cuando la gente piensa que está más definida de lo que realmente está.

Lo que escribió Kent Beck en Test Driven Development, By Example

  

Los llamo "pruebas unitarias", pero no coinciden muy bien con la definición aceptada de pruebas unitarias

Cualquier reclamo dado de "el punto de las pruebas unitarias es" dependerá en gran medida de la definición de "prueba unitaria" que 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 restringe a un estilo que prueba cada unidad de forma aislada, entonces una gran cantidad de test doubles es una conclusión inevitable.

El consejo conflictivo 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 debería admitirse, entonces es necesario dar algo. Tal vez este tipo de prueba necesita un nombre diferente? O tal vez necesitamos una comprensión diferente de "unidad".

Es posible que desee comparar:

  

¿Una prueba que prueba el método de cálculo de la persona sin burlarse de la dependencia de la Calculadora (dado que la Calculadora es una clase liviana que no accede al "mundo exterior") puede considerarse una prueba unitaria?

Creo que esa es la pregunta incorrecta; Es nuevamente un argumento sobre etiquetas , cuando creo que lo que realmente nos importa son propiedades .

Cuando estoy introduciendo cambios en el código, no me importa el aislamiento de las pruebas; ya sé que "el error" se encuentra 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 es valioso 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 300ms , 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 lo hace. Proporcionar beneficios que superen los costos.

Observe las dos suposiciones aquí: un ser humano como parte de la evaluación de costos y la pequeña cantidad de cambios no verificados en la evaluación de beneficios. En circunstancias en las que esas condiciones no se cumplen, el valor de "aislamiento" cambia bastante.

Vea también Hot Lava , por Harry Percival.

    
respondido por el VoiceOfUnreason 27.11.2018 - 12:47
32
  

¿Cómo deben escribirse exactamente las pruebas unitarias sin burlarse ampliamente?

Al minimizar los efectos secundarios en tu 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 la capacidad de interactuar con esa API web o crea una simulación de la misma. Sin embargo, si es un conjunto de funciones de cálculo determinista y sin estado, entonces usted no (y no debería) se burla de él. Si lo haces, te arriesgas a que tu simulacro se comporte de manera diferente al código real, lo que lleva a errores en tus pruebas.

Las simulaciones solo deben ser necesarias 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 estatal y no determinista. Así que si mantienes esas partes del código al mínimo y las ocultas tras las abstracciones, entonces son fáciles de burlar y el resto de tu código evita la necesidad de simulacros.

Para los puntos de código que sí tienen efectos secundarios, vale la pena escribir pruebas que simulen y pruebas que no. Los últimos, aunque necesitan atención, ya que serán inherentemente frágiles y posiblemente lentos. Por lo tanto, es posible que desee ejecutarlos solo durante la noche en un servidor CI, en lugar de cada vez que guarde y genere su código. Sin embargo, las pruebas anteriores deben ejecutarse tan a menudo como sea posible. En cuanto a si cada prueba es, entonces, una prueba de unidad o integración se convierte en académica y evita "guerras de fuego" sobre lo que es y no es una prueba de unidad.

    
respondido por el David Arno 27.11.2018 - 13:34
30

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 lo necesario para lograr ese objetivo. Puede ser necesario burlarse, pero no es el punto de la prueba. Una prueba de integración prueba la interacción entre diferentes unidades reales . Esta diferencia es la razón principal por la que necesitamos tanto la unidad como las pruebas de integración. Si uno hizo el trabajo del otro 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 debería hacer una prueba de unidad? Como se dijo anteriormente, las pruebas unitarias deben construir estructuras auxiliares solo en la medida en que sea necesario . A menudo es más fácil utilizar una base de datos simulada que su base de datos real o incluso cualquier base de datos real. Sin embargo, la burla en sí misma no tiene ningún valor. Si a menudo sucede, es más fácil utilizar los componentes reales de otra capa como entrada para una prueba unitaria de nivel medio. Si es así, no dudes en usarlos.

Muchos practicantes 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 varios lugares. Considero que esto no es un problema: un conjunto de pruebas debe tener éxito 100% para brindarle la tranquilidad que necesita, por lo que no es un gran problema tener demasiados errores: después de todo, usted < em> do tiene un defecto. El único problema crítico sería si un defecto desencadenó demasiadas fallas pocas .

Por lo tanto, no hagas una religión de burla. Es un medio, no un fin, por lo que si puede evitar el esfuerzo extra, debe hacerlo.

    
respondido por el Kilian Foth 27.11.2018 - 11:59
4

Bien, para responder directamente a sus preguntas:

  

¿Cómo deben escribirse correctamente las pruebas unitarias?

Como usted dice, debe burlarse de las dependencias y probar solo la unidad en cuestión.

  

¿Dónde se encuentra exactamente la línea entre ellos y las pruebas de integración?

Una prueba de integración es una prueba unitaria en la que no se burlan de sus dependencias.

  

¿Puede una prueba que pruebe el método Person.calculate sin burlarse de   Calculadora se considera una prueba unitaria?

No. Necesita inyectar la dependencia de la calculadora en este código y puede elegir entre una versión simulada o una versión real. Si usas uno simulado, es una prueba unitaria, si usas uno real, es una prueba de integración.

Sin embargo, una advertencia. ¿realmente te importa cómo piensan las personas que deberían llamarse tus pruebas?

Pero tu verdadera pregunta parece ser esta:

  

una rápida búsqueda en Google acerca de la burla revela toneladas de artículos que   Afirmar que "burlarse es un olor de código" y debería en su mayoría (aunque no   completamente) ser evitado.

Creo que el problema aquí es que muchas personas utilizan simulacros para recrear completamente las dependencias. Por ejemplo, podría burlarse de la calculadora en su ejemplo como

public class MockCalc : ICalculator
{
     public Add(int a, int b) { return 4; }
}

No haría algo como:

myMock = Mock<ICalculator>().Add((a,b) => {return a + b;})
myPerson.Calculate()
Assert.WasCalled(myMock.Add());

Yo diría que eso sería "probar mi simulacro" o "probar la implementación". Yo diría " ¡No escribas Mocks! * así".

Otras personas no estarían de acuerdo conmigo, comenzaríamos guerras de fuego masivas en nuestros blogs sobre la Mejor manera de simularse, lo que realmente no tendría sentido a menos que comprendiera todo el trasfondo de los diversos enfoques y realmente no ofrezca un todo Mucho valor para alguien que solo quiere escribir buenas pruebas.

    
respondido por el Ewan 27.11.2018 - 13:12
3
  
  1. ¿Cómo deben implementarse correctamente las pruebas unitarias?
  2.   

Mi regla de oro es que las pruebas de unidad adecuadas:

  • Están codificados contra interfaces, no implementaciones . Esto tiene muchos beneficios. Por un lado, garantiza que sus clases sigan el Principio de inversión de dependencia de SOLID . Además, esto es lo que hacen tus otras clases ( ¿verdad? ) por lo que tus pruebas deberían hacer lo mismo. Además, esto le permite probar múltiples implementaciones de la misma interfaz mientras reutiliza gran parte del código de prueba (solo cambiarían la inicialización y algunas aserciones).
  • Son autónomos . Como dijo, los cambios en cualquier código externo no pueden afectar el resultado de la prueba. Como tal, las pruebas unitarias pueden ejecutarse en tiempo de construcción. Esto significa que necesitas simulacros para eliminar cualquier efecto secundario. Sin embargo, si está siguiendo el principio de inversión de dependencia, esto debería ser relativamente fácil. Se pueden utilizar buenos marcos de prueba como Spock para proporcionar dinámicamente implementaciones simuladas de cualquier interfaz para usar en sus pruebas con Codificación mínima. Esto significa que cada clase de prueba solo necesita ejercer código desde exactamente una clase de implementación, más el marco de prueba (y quizás las clases modelo ["beans")).
  • No requiere una aplicación que se ejecute por separado . Si la prueba necesita "hablar con algo", ya sea una base de datos o un servicio web, es una prueba de integración, no una prueba unitaria. Trazo la línea en las conexiones de red o en el sistema de archivos. Una base de datos SQLite puramente en memoria, por ejemplo, es un juego justo en mi opinión para una prueba unitaria si realmente la necesita.

Si hay clases de utilidad de los marcos que complican las pruebas unitarias, puede que incluso le resulte útil crear interfaces y clases muy simples para facilitar el simulacro de esas dependencias. Esos envoltorios no estarían necesariamente sujetos a pruebas de unidad.

  
  1. ¿Dónde está exactamente la línea entre ellos [pruebas de unidad] y las pruebas de integración?
  2.   

He encontrado que esta distinción es la más útil:

  • Las pruebas unitarias simulan el "código de usuario" , verificando el comportamiento de las clases de implementación contra el comportamiento deseado y la semántica de interfaces de nivel de código .
  • Las pruebas de integración simulan al usuario , verificando el comportamiento de la aplicación en ejecución frente a casos de uso específicos y / o API formales. Para un servicio web, el "usuario" sería una aplicación cliente.

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 luego destruir el contenedor, ¿está bien incluir esas pruebas como "pruebas unitarias"? Si este es tu debate candente, estás en un lugar bastante bueno.

  
  1. ¿Es cierto que prácticamente todas las pruebas de unidad deben simularse?
  2.   

No. Algunos casos de prueba individuales serán por condiciones de error, como pasar null como parámetro y verificar que obtenga una excepción. Un montón de pruebas como esa no requerirán ninguna burla. Además, las implementaciones que no tienen efectos secundarios, por ejemplo, el procesamiento de cadenas o las funciones matemáticas, pueden no requerir ningún simulacro porque simplemente verifica la salida. Pero creo que la mayoría de las clases que vale la pena tener requerirán al menos un simulacro en algún lugar del código de prueba. (Cuanto menos, mejor.)

El problema del "olor a código" que mencionaste surge cuando tienes una clase que es demasiado complicada, que requiere una larga lista de dependencias simuladas para escribir tus 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ácil de probar. Esto mejorará la calidad a largo plazo.

  

Solo una prueba de unidad debe interrumpirse por un error en la unidad probada.

No creo que esta sea una expectativa razonable, porque funciona en contra de la reutilización. Puede tener un método private , por ejemplo, que se invoca mediante varios métodos public 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 cada método public .

    
respondido por el wberry 28.11.2018 - 00:28
3
  
  1. No deben interrumpir por ningún código no relacionado en otra parte del código base.
  2.   

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, colaboradores, y no están relacionadas. Si sus pruebas se rompen y su código de producción no lo hace, entonces sus pruebas son sospechosas.

  
  1. Solo una prueba de unidad debe interrumpirse por un error en la unidad probada, a diferencia de las pruebas de integración (que pueden romperse en montones).
  2.   

Yo también consideraría esta regla con sospecha. Si eres lo suficientemente bueno como para estructurar tu código y escribir tus pruebas de modo que un error cause exactamente una falla de prueba de una 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 los casos que no he anticipado

  

¿Dónde se encuentra exactamente la línea entre ellos y las pruebas de integración?

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 del dominio de problemas / reglas de negocios que trata ese nivel del código. A menudo, estas pruebas son de alguna manera "funcionales": se ponen en una entrada y se prueba 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 refacta.

  

¿Cómo deben escribirse exactamente las pruebas unitarias sin burlarse ampliamente?

No lea demasiado en la palabra 'unidad', e inclínese hacia el uso de sus clases reales de producción en las pruebas, sin preocuparse demasiado si involucra a más de una de ellas en una prueba. Si uno de ellos es difícil de usar (porque requiere mucha inicialización, o necesita llegar a una base de datos / servidor de correo electrónico real, etc.), entonces deje que sus pensamientos se conviertan en burla / simulación.

    
respondido por el topo morto 28.11.2018 - 00:55

Lea otras preguntas en las etiquetas