Algunas personas sostienen que las pruebas de integración son malas y están mal : todo debe ser probado por la unidad, lo que significa que debes burlarte de las dependencias; una opción que, por varias razones, no siempre me gusta.
Creo que, en algunos casos, una prueba unitaria simplemente no prueba nada.
Tomemos como ejemplo la siguiente implementación de repositorio (trivial, ingenua) (en PHP):
class ProductRepository
{
private $db;
public function __construct(ConnectionInterface $db) {
$this->db = $db;
}
public function findByKeyword($keyword) {
// this might have a query builder, keyword processing, etc. - this is
// a totally naive example just to illustrate the DB dependency, mkay?
return $this->db->fetch("SELECT * FROM products p"
. " WHERE p.name LIKE :keyword", ['keyword' => $keyword]);
}
}
Digamos que quiero demostrar en una prueba que este repositorio realmente puede encontrar productos que coincidan con varias palabras clave dadas.
A falta de pruebas de integración con un objeto de conexión real, ¿cómo puedo saber que esto realmente está generando consultas reales y que esas consultas realmente hacen lo que creo que hacen?
Si tengo que burlarme del objeto de conexión en una prueba unitaria, solo puedo probar cosas como "genera la consulta esperada", pero eso no significa que realmente vaya a funcionar ... es decir, tal vez esté generando la consulta Lo esperaba, pero tal vez esa consulta no hace lo que creo que hace.
En otras palabras, siento que una prueba que hace afirmaciones sobre la consulta generada, esencialmente no tiene valor, porque está probando cómo findByKeyword()
se implementó el método , pero eso no prueba que realmente funcione .
Este problema no se limita a los repositorios o la integración de la base de datos: parece aplicarse en muchos casos, donde hacer afirmaciones sobre el uso de un simulacro (prueba doble) solo prueba cómo se implementan las cosas, no si van a En realidad funciona.
¿Cómo lidias con situaciones como estas?
¿Las pruebas de integración son realmente "malas" en un caso como este?
Entiendo que es mejor probar una cosa, y también entiendo por qué las pruebas de integración conducen a innumerables rutas de código, todas las cuales no se pueden probar, pero en el caso de un servicio (como un repositorio) cuyo único propósito es para interactuar con otro componente, ¿cómo puede realmente probar algo sin pruebas de integración?
fuente
Respuestas:
Su compañero de trabajo tiene razón en que todo lo que puede ser probado en una unidad debe ser probado en una unidad, y usted tiene razón en que las pruebas unitarias lo llevarán solo hasta cierto punto y no más allá, particularmente al escribir envoltorios simples alrededor de servicios externos complejos.
Una forma común de pensar acerca de las pruebas es como una pirámide de pruebas . Es un concepto frecuentemente conectado con Agile, y muchos han escrito sobre él, incluido Martin Fowler (quien lo atribuye a Mike Cohn en Succeeding with Agile ), Alistair Scott y el blog de Google Testing .
La idea es que las pruebas unitarias rápidas y resistentes son la base del proceso de prueba: debe haber más pruebas unitarias enfocadas que las pruebas de sistema / integración, y más pruebas de sistema / integración que las pruebas de extremo a extremo. A medida que se acerca a la cima, las pruebas tienden a tomar más tiempo / recursos para ejecutarse, tienden a estar más frágiles y escamosas, y son menos específicas para identificar qué sistema o archivo está dañado ; naturalmente, es preferible evitar ser "muy pesado".
Hasta ese momento, las pruebas de integración no son malas , pero una gran dependencia de ellas puede indicar que no ha diseñado sus componentes individuales para que sean fáciles de probar. Recuerde, el objetivo aquí es probar que su unidad está funcionando según sus especificaciones mientras involucra un mínimo de otros sistemas rompibles : es posible que desee probar una base de datos en memoria (que cuento como una prueba de prueba de unidad doble junto con simulacros ) para pruebas de casos extremos pesados, por ejemplo, y luego escriba un par de pruebas de integración con el motor de base de datos real para establecer que los casos principales funcionan cuando se ensambla el sistema.
Como nota al margen, mencionó que los simulacros que escribe simplemente prueban cómo se implementa algo, no si funciona . Eso es algo así como un antipatrón: una prueba que es un espejo perfecto de su implementación en realidad no está probando nada en absoluto. En cambio, pruebe que cada clase o método se comporte de acuerdo con sus propias especificaciones , en cualquier nivel de abstracción o realismo que requiera.
fuente
Eso es un poco como decir que los antibióticos son malos: todo debe curarse con vitaminas.
Las pruebas unitarias no pueden atrapar todo, solo prueban cómo funciona un componente en un entorno controlado . Las pruebas de integración verifican que todo funciona en conjunto , lo cual es más difícil de hacer pero más significativo al final.
Un buen proceso de prueba integral utiliza ambos tipos de pruebas: pruebas unitarias para verificar las reglas comerciales y otras cosas que se pueden probar de forma independiente, y pruebas de integración para asegurarse de que todo funcione en conjunto.
Usted podría unidad de probarlo en el nivel de base de datos . Ejecute la consulta con varios parámetros y vea si obtiene los resultados que espera. De acuerdo, significa copiar / pegar cualquier cambio en el código "verdadero". pero no le permiten probar la consulta independiente de cualesquiera otras dependencias.
fuente
Las pruebas unitarias no detectan todos los defectos. Pero son más baratos de configurar y (re) ejecutar en comparación con otros tipos de pruebas. Las pruebas unitarias están justificadas por la combinación de valor moderado y costo bajo a moderado.
Aquí hay una tabla que muestra las tasas de detección de defectos para diferentes tipos de pruebas.
fuente: p.470 en Code Complete 2 por McConnell
fuente
No, no son malos. Con suerte, uno debería tener pruebas de unidad e integración. Se utilizan y se ejecutan en diferentes etapas del ciclo de desarrollo.
Pruebas unitarias
Las pruebas unitarias deben ejecutarse en el servidor de compilación y localmente, después de compilar el código. Si falla alguna de las pruebas unitarias, se debe fallar la compilación o no confirmar la actualización del código hasta que se corrijan las pruebas. La razón por la que queremos aislar las pruebas unitarias es que queremos que el servidor de compilación pueda ejecutar todas las pruebas sin todas las dependencias. Entonces podríamos ejecutar la compilación sin todas las dependencias complejas requeridas y tener muchas pruebas que se ejecutan muy rápido.
Entonces, para una base de datos, uno debería tener algo como:
Ahora, la implementación real de IRepository irá a la base de datos para obtener los productos, pero para las pruebas unitarias, uno puede burlarse de IRepository con uno falso para ejecutar todas las pruebas según sea necesario sin una base de datos actaul, ya que podemos simular todo tipo de listas de productos devuelto desde la instancia simulada y pruebe cualquier lógica de negocios con los datos simulados
Pruebas de integración
Las pruebas de integración son típicamente pruebas de cruce de límites. Queremos ejecutar estas pruebas en el servidor de implementación (el entorno real), sandbox o incluso localmente (señalado a sandbox). No se ejecutan en el servidor de compilación. Después de que el software se haya implementado en el entorno, normalmente se ejecutará como actividad posterior a la implementación. Se pueden automatizar mediante utilidades de línea de comandos. Por ejemplo, podemos ejecutar nUnit desde la línea de comandos si clasificamos todas las pruebas de integración que queremos invocar. Estos realmente llaman al repositorio real con la llamada a la base de datos real. Este tipo de pruebas ayudan con:
Estas pruebas a veces son más difíciles de ejecutar, ya que es posible que necesitemos configurarlas y / o derribarlas también. Considere agregar un producto. Probablemente deseamos agregar el producto, consultarlo para ver si se agregó y luego, una vez que hayamos terminado, eliminarlo. No queremos agregar cientos o miles de productos de "integración", por lo que se requiere una configuración adicional.
Las pruebas de integración pueden resultar muy valiosas para validar un entorno y asegurarse de que lo real funcione.
Uno debería tener ambos.
fuente
Las pruebas de integración de bases de datos no son malas. Aún más, son necesarios.
Probablemente tenga su aplicación dividida en capas, y es algo bueno. Puede probar cada capa de forma aislada burlándose de las capas vecinas, y eso también es bueno. Pero no importa cuántas capas de abstracción crees, en algún momento debe haber una capa que haga el trabajo sucio, en realidad hablar con la base de datos. A menos que lo pruebe, no lo hace en absoluto. Si prueba la capa n burlándose de la capa n-1 , está evaluando la suposición de que la capa n funciona con la condición de que la capa n-1 funcione. Para que esto funcione, de alguna manera debes probar que la capa 0 funciona.
Si bien en teoría podría unir la base de datos de prueba, analizando e interpretando el SQL generado, es mucho más fácil y confiable crear una base de datos de prueba sobre la marcha y hablar con ella.
Conclusión
¿Cuál es la confianza obtenida de la unidad que prueba sus capas de repositorio abstracto , etéreo objeto-relacional-mapeador , registro activo genérico , persistencia teórica , cuando al final su SQL generado contiene un error de sintaxis?
fuente
Necesitas ambos.
En su ejemplo, si estaba probando que una base de datos en una determinada condición, cuando
findByKeyword
se ejecuta el método, recupera los datos que espera que sean una prueba de integración excelente.En cualquier otro código que esté usando ese
findByKeyword
método, desea controlar lo que se está alimentando a la prueba, para que pueda devolver valores nulos o las palabras correctas para su prueba o lo que sea, luego se burla de la dependencia de la base de datos para que sepa exactamente lo que hará su prueba recibir (y pierde la sobrecarga de conectarse a una base de datos y asegurarse de que los datos que contiene son correctos)fuente
El autor del artículo de blog al que se refiere está principalmente preocupado por la complejidad potencial que puede surgir de las pruebas integradas (aunque está escrito de una manera muy cauta y categórica). Sin embargo, las pruebas integradas no son necesariamente malas, y algunas en realidad son más útiles que las pruebas unitarias puras. Realmente depende del contexto de su aplicación y de lo que está tratando de probar.
Muchas aplicaciones de hoy simplemente no funcionarían si su servidor de base de datos dejara de funcionar. Al menos, piénselo en el contexto de la función que está intentando probar.
Por un lado, si lo que está tratando de probar no depende, o puede hacerse que no dependa en absoluto, de la base de datos, escriba su prueba de tal manera que ni siquiera intente usar el base de datos (solo proporcione datos simulados según sea necesario). Por ejemplo, si está tratando de probar alguna lógica de autenticación cuando sirve una página web (por ejemplo), probablemente sea bueno separar eso del DB por completo (suponiendo que no confíe en el DB para la autenticación, o eso puedes burlarte de manera razonablemente fácil).
Por otro lado, si es una característica que depende directamente de su base de datos y que no funcionaría en un entorno real si la base de datos no estuviera disponible, entonces se burla de lo que hace la base de datos en su código de cliente de base de datos (es decir, la capa que usa esa DB) no necesariamente tiene sentido.
Por ejemplo, si sabe que su aplicación va a depender de una base de datos (y posiblemente de un sistema de base de datos específico), burlarse del comportamiento de la base de datos en aras de la misma a menudo será una pérdida de tiempo. Los motores de base de datos (especialmente RDBMS) son sistemas complejos. Unas pocas líneas de SQL pueden realizar mucho trabajo, lo que sería difícil de simular (de hecho, si su consulta SQL es de unas pocas líneas, es probable que necesite muchas más líneas de Java / PHP / C # / Python código para producir el mismo resultado internamente): duplicar la lógica que ya ha implementado en la base de datos no tiene sentido, y verificar ese código de prueba se convertiría en un problema en sí mismo.
No necesariamente trataría esto como un problema de prueba unitaria versus prueba integrada , sino que miro el alcance de lo que se está probando. Los problemas generales de las pruebas de unidad e integración permanecen: necesita un conjunto razonablemente realista de datos de prueba y casos de prueba, pero algo que también sea lo suficientemente pequeño como para que las pruebas se ejecuten rápidamente.
El tiempo para restablecer la base de datos y repoblar con datos de prueba es un aspecto a considerar; generalmente evaluaría esto en función del tiempo que lleva escribir ese código simulado (que eventualmente tendría que mantener también).
Otro punto a considerar es el grado de dependencia que tiene su aplicación con la base de datos.
fuente
Tienes razón al pensar que tal prueba unitaria está incompleta. Lo incompleto está en la interfaz de la base de datos que se está burlando. La expectativa o afirmaciones ingenuas de este tipo son incompletas.
Para completarlo, tendría que dedicar suficiente tiempo y recursos para escribir o integrar un motor de reglas SQL que garantice que la instrucción SQL emitida por el sujeto bajo prueba, resulte en las operaciones esperadas.
Sin embargo, la alternativa / acompañante a menudo olvidada y algo costosa a la burla es la "virtualización" .
¿Puede activar una instancia de base de datos temporal, en memoria pero "real" para probar una sola función? si ? allí, tiene una mejor prueba, la que sí verifica los datos reales guardados y recuperados.
Ahora, uno podría decir, convirtió una prueba unitaria en una prueba de integración. Existen diferentes puntos de vista sobre dónde dibujar la línea para clasificar entre pruebas unitarias y pruebas de integración. En mi humilde opinión, "unidad" es una definición arbitraria y debe adaptarse a sus necesidades.
fuente
Unit Tests
yIntegration Tests
son ortogonales entre sí. Ofrecen una vista diferente de la aplicación que está creando. Usualmente quieres los dos . Pero el momento en el tiempo difiere, cuando quieres qué tipo de pruebas.Lo que más a menudo quieras
Unit Tests
. Las pruebas unitarias se centran en una pequeña porción del código que se está probando; lo que se llama exactamente aunit
se deja al lector. Pero el propósito es simple: obtener comentarios rápidos sobre cuándo y dónde se rompió su código . Dicho esto, debe quedar claro, que las llamadas a una base de datos real son un no .Por otro lado, hay cosas que solo se pueden probar en condiciones difíciles sin una base de datos. Quizás haya una condición de carrera en su código y una llamada a un DB arroje una violación de una
unique constraint
que solo podría lanzarse si realmente usa su sistema. Pero ese tipo de pruebas son costosas que no puede (y no quiere) ejecutarlas con tanta frecuenciaunit tests
.fuente
En el mundo .Net tengo la costumbre de crear un proyecto de prueba y crear pruebas como método de codificación / depuración / prueba de ida y vuelta menos la IU. Esta es una forma eficiente de desarrollarme. No estaba tan interesado en ejecutar todas las pruebas para cada compilación (porque ralentiza mi flujo de trabajo de desarrollo), pero entiendo la utilidad de esto para un equipo más grande. Sin embargo, puede establecer una regla que, antes de confirmar el código, todas las pruebas se ejecuten y aprueben (si las pruebas tardan más en ejecutarse porque la base de datos está siendo atacada).
Mockear la capa de acceso a datos (DAO) y no llegar a la base de datos, no solo no me permite codificar de la manera que me gusta y me he acostumbrado, sino que pierde una gran parte de la base de código real. Si realmente no está probando la capa de acceso a datos y la base de datos y solo está fingiendo, y luego pasa mucho tiempo burlándose de las cosas, no entiendo la utilidad de este enfoque para probar realmente mi código. Estoy probando una pieza pequeña en lugar de una más grande con una prueba. Entiendo que mi enfoque podría estar más en la línea de una prueba de integración, pero parece que la prueba unitaria con el simulacro es una pérdida de tiempo redundante si en realidad solo escribe la prueba de integración una vez y primero. También es una buena forma de desarrollar y depurar.
De hecho, desde hace un tiempo conozco TDD y Behavior Driven Design (BDD) y pienso en formas de usarlo, pero es difícil agregar pruebas unitarias retroactivamente. Quizás estoy equivocado, pero escribir una prueba que cubra más código de principio a fin con la base de datos incluida, parece una prueba mucho más completa y de mayor prioridad para escribir que cubre más código y es una forma más eficiente de escribir pruebas.
De hecho, creo que algo como Behavior Driven Design (BDD) que intenta probar de principio a fin con un lenguaje específico de dominio (DSL) debería ser el camino a seguir. Tenemos SpecFlow en el mundo .Net, pero comenzó como código abierto con Cucumber.
https://cucumber.io/
Realmente no estoy impresionado con la verdadera utilidad de la prueba que escribí burlándose de la capa de acceso a datos y sin llegar a la base de datos. El objeto devuelto no alcanzó la base de datos y no se rellenó con datos. Era un objeto completamente vacío que tuve que burlarme de una manera antinatural. Solo creo que es una pérdida de tiempo.
Según Stack Overflow, la burla se usa cuando los objetos reales no son prácticos para incorporar en la prueba de la unidad.
https://stackoverflow.com/questions/2665812/what-is-mocking
"La burla se usa principalmente en pruebas unitarias. Un objeto bajo prueba puede tener dependencias de otros objetos (complejos). Para aislar el comportamiento del objeto que desea probar, reemplace los otros objetos por simulacros que simulan el comportamiento de los objetos reales. Esto es útil si los objetos reales no son prácticos para incorporar en la prueba unitaria ".
Mi argumento es que si estoy codificando algo de extremo a extremo (interfaz de usuario web a capa empresarial a capa de acceso a datos a base de datos, ida y vuelta), antes de registrar algo como desarrollador, voy a probar este flujo de ida y vuelta. Si elimino la interfaz de usuario y depuro y pruebo este flujo a partir de una prueba, estoy probando todo menos la interfaz de usuario y devolviendo exactamente lo que la interfaz de usuario espera. Todo lo que me queda es enviar a la interfaz de usuario lo que quiere.
Tengo una prueba más completa que es parte de mi flujo de trabajo de desarrollo natural. Para mí, esa debería ser la prueba de mayor prioridad que cubra probar la especificación real del usuario de principio a fin tanto como sea posible. Si nunca creo otras pruebas más granulares, al menos tengo esta prueba más completa que demuestra que mi funcionalidad deseada funciona.
Un cofundador de Stack Exchange no está convencido de los beneficios de tener una cobertura de prueba de unidad del 100%. Yo tampoco lo soy. Tomaría una "prueba de integración" más completa que afecta a la base de datos en lugar de mantener un montón de simulaciones de bases de datos cualquier día.
https://www.joelonsoftware.com/2009/01/31/from-podcast-38/
fuente
Las dependencias externas se deben burlar porque no puede controlarlas (pueden pasar durante la fase de prueba de integración pero fallar en la producción). Las unidades pueden fallar, las conexiones de la base de datos pueden fallar por varias razones, podría haber problemas de red, etc. Tener pruebas de integración no da ninguna confianza adicional porque son todos problemas que pueden ocurrir en tiempo de ejecución.
Con las pruebas unitarias verdaderas, está probando dentro de los límites de la caja de arena y debería estar claro. Si un desarrollador escribió una consulta SQL que falló en QA / PROD, significa que ni siquiera lo probaron una vez antes de ese momento.
fuente