¿Las pruebas de integración están destinadas a repetir todas las pruebas unitarias?

37

Digamos que tengo una función (escrita en Ruby, pero todos deberían entenderla):

def am_I_old_enough?(name = 'filip')
   person = Person::API.new(name)
   if person.male?
      return person.age > 21
   else
      return person.age > 18
   end
end

En las pruebas unitarias, crearía cuatro pruebas para cubrir todos los escenarios. Cada uno usará un Person::APIobjeto burlado con métodos male?y trozos age.

Ahora se trata de escribir pruebas de integración. Supongo que Person :: API ya no debe ser burlado. Por lo tanto, crearía exactamente los mismos cuatro casos de prueba, pero sin burlarse del objeto Person :: API. ¿Es eso correcto?

En caso afirmativo, ¿cuál es el punto de escribir pruebas unitarias, si pudiera escribir pruebas de integración que me den más confianza (ya que trabajo en objetos reales, no en trozos o simulacros)?

Filip Bartuzi
fuente
3
Bueno, uno de los puntos es que al burlarse / probarlo unitariamente, puede aislar cualquier problema en su código. Si falla una prueba de integración, no sabe qué código está roto, el suyo o la API.
Chris Wohlert
99
¿Solo cuatro pruebas? Tienes seis edades límite que deberías probar: 17, 18, 19, 20, 21, 22 ...;)
David Arno
22
@FilipBartuzi, supongo que el método está verificando si un hombre tiene más de 21 años, por ejemplo. Como está escrito actualmente, no hace eso, solo es cierto si tienen más de 22 años. "Más de 21" en inglés significa "21+". Entonces hay un error en su código. Dichos errores se capturan mediante la prueba de valores límite, es decir, 20, 21, 22 para un hombre, 17,18,19 para una mujer en este caso. Por lo tanto, se necesitan al menos seis pruebas.
David Arno
66
Sin mencionar los casos de 0 y -1. ¿Qué significa para una persona tener -1 años de edad? ¿Qué debe hacer su código si su API devuelve algo sin sentido?
RubberDuck
99
Esto sería mucho más fácil de probar si pasó un objeto de persona como parámetro.
JeffO

Respuestas:

72

No, las pruebas de integración no deberían simplemente duplicar la cobertura de las pruebas unitarias. Ellos pueden duplicar algún tipo de cobertura, pero ese no es el punto.

El objetivo de una prueba unitaria es garantizar que un pequeño bit específico de funcionalidad funcione de manera exacta y completa según lo previsto. Una prueba unitaria para am_i_old_enoughprobar datos con diferentes edades, ciertamente las cercanas al umbral, posiblemente todas las edades humanas. Después de haber escrito esta prueba, la integridad de am_i_old_enoughnunca debe volver a estar en duda.

El objetivo de una prueba de integración es verificar que todo el sistema, o una combinación de un número sustancial de componentes, haga lo correcto cuando se usan juntos . Al cliente no le importa una función de utilidad en particular que haya escrito, le importa que su aplicación web esté debidamente protegida contra el acceso de menores, porque de lo contrario los reguladores tendrán su trasero.

Verificar la edad del usuario es una pequeña parte de esa funcionalidad, pero la prueba de integración no verifica si su función de utilidad utiliza el valor umbral correcto. Comprueba si la persona que llama toma la decisión correcta en función de ese umbral, si se llama a la función de utilidad, si se cumplen otras condiciones de acceso, etc.

La razón por la que necesitamos ambos tipos de pruebas es básicamente que hay una explosión combinatoria de posibles escenarios para la ruta a través de una base de código que puede tomar la ejecución. Si la función de utilidad tiene aproximadamente 100 entradas posibles, y hay cientos de funciones de utilidad, entonces verificar que suceda lo correcto en todos los casos requeriría muchos, muchos millones de casos de prueba. Simplemente verificando todos los casos en ámbitos muy pequeños y luego verificando combinaciones comunes, relevantes o probables de estos ámbitos, mientras se supone que estos ámbitos pequeños ya son correctos, como lo demuestran las pruebas unitarias , podemos obtener una evaluación bastante segura de que el sistema está haciendo lo que debería, sin ahogarse en escenarios alternativos para probar.

Kilian Foth
fuente
66
"Podemos obtener una evaluación bastante segura de que el sistema está haciendo lo que debería, sin ahogarse en escenarios alternativos para probar". Gracias. Me encanta cuando alguien se acerca a las pruebas automatizadas con sensatez.
jpmc26
1
JB Rainsberger tiene una buena charla sobre las pruebas y la explosión combinatoria sobre la que está escribiendo en el último párrafo, llamada "Las pruebas integradas son una estafa" . No se trata tanto de pruebas de integración, pero sigue siendo bastante interesante.
Bart van Nierop
The customer doesn't care about a particular utility function you wrote, they care that their web app is properly secured against access by minors-> Esa es una mentalidad muy inteligente, ¡gracias! El problema es cuando haces un proyecto por ti mismo. Es difícil dividir su mentalidad entre ser un programador y ser un gerente de producto en el mismo momento
Filip Bartuzi
14

La respuesta corta es no". La parte más interesante es por qué / cómo podría surgir esta situación.

Creo que la confusión está surgiendo porque está tratando de adherirse a prácticas de prueba estrictas (pruebas unitarias versus pruebas de integración, burlas, etc.) para código que no parece adherirse a prácticas estrictas.

Eso no quiere decir que el código sea "incorrecto", o que las prácticas particulares sean mejores que otras. Simplemente que algunas de las suposiciones hechas por las prácticas de prueba pueden no aplicarse en esta situación, y puede ayudar usar un nivel similar de "rigor" en las prácticas de codificación y prueba; o al menos, reconocer que pueden estar desequilibrados, lo que hará que algunos aspectos sean inaplicables o redundantes.

La razón más obvia es que su función está realizando dos tareas diferentes:

  • Buscando un Personbasado en su nombre. Esto requiere pruebas de integración, para asegurarse de que pueda encontrarPerson objetos que presumiblemente se crean / almacenan en otro lugar.
  • Calcular si a Persones lo suficientemente mayor, en función de su género. Esto requiere pruebas unitarias, para asegurarse de que el cálculo funcione como se esperaba.

Al agrupar estas tareas en un bloque de código, no puede ejecutar una sin la otra. Cuando desea realizar una prueba unitaria de los cálculos, se ve obligado a buscar unPerson (ya sea desde una base de datos real o desde un trozo / simulacro). Cuando desee comprobar que la búsqueda se integra con el resto del sistema, también se ve obligado a realizar un cálculo de la edad. ¿Qué debemos hacer con ese cálculo? ¿Deberíamos ignorarlo o verificarlo? Esa parece ser la situación exacta que estás describiendo en tu pregunta.

Si imaginamos una alternativa, podríamos tener el cálculo por sí solo:

def is_old_enough?(person)
   if person.male?
      return person.age > 21
   else 
      return person.age > 18
   end
end

Dado que este es un cálculo puro, no necesitamos realizar pruebas de integración en él.

También podríamos sentir la tentación de escribir la tarea de búsqueda por separado también:

def person_from_name(name = 'filip')
   return Person::API.new(name)
end

Sin embargo, en este caso, la funcionalidad es tan cercana Person::API.newque diría que debería usarla en su lugar (si el nombre predeterminado es necesario, ¿estaría mejor almacenado en otro lugar, como un atributo de clase?).

Al escribir pruebas de integración para Person::API.new(o person_from_name) todo lo que necesita preocuparse es si recupera lo esperado Person; todos los cálculos basados ​​en la edad se realizan en otro lugar, por lo que sus pruebas de integración pueden ignorarlos.

Warbo
fuente
11

Otro punto que me gusta agregar a la respuesta de Killian es que las pruebas unitarias se ejecutan muy rápidamente, por lo que podemos tener miles de ellas. Una prueba de integración generalmente toma más tiempo porque está llamando a servicios web, bases de datos o alguna otra dependencia externa, por lo que no podemos ejecutar las mismas pruebas (1000) para escenarios de integración, ya que tomarían demasiado tiempo.

Además, la unidad de pruebas suelen obtener funcionar a la acumulación de tiempo (en la construcción de la máquina) y pruebas de integración ejecuta después el despliegue en un entorno / máquina.

Por lo general, se ejecutarían nuestras miles de pruebas unitarias para cada compilación, y luego nuestras más o menos 100 pruebas de integración de alto valor después de cada implementación. Es posible que no llevemos cada compilación a la implementación, pero eso está bien porque la compilación que tomamos para la implementación se ejecutarán las pruebas de integración. Por lo general, queremos limitar estas pruebas para que se ejecuten en 10 o 15 minutos porque no queremos demorar demasiado la implementación.

Además, en un horario semanal podemos ejecutar un conjunto de regresión de pruebas de integración que cubren más escenarios durante el fin de semana u otros períodos de inactividad. Estos pueden tomar más de 15 minutos, ya que se cubrirán más escenarios, pero generalmente nadie está trabajando en sábado / domingo, por lo que podemos tomar más tiempo con las pruebas.

Jon Raynor
fuente
no se aplica a lenguajes dinámicos (es decir, sin etapa de construcción)
Filip Bartuzi