Pruebas unitarias y bases de datos: ¿en qué momento me conecto realmente a la base de datos?

37

Hay respuestas a la pregunta sobre cómo las clases de prueba que se conectan a una base de datos, por ejemplo, "¿Deberían conectarse las clases de prueba de servicio ..." y "Prueba de unidad - Aplicación acoplada a la base de datos" .

Entonces, en resumen, supongamos que tiene una clase A que necesita conectarse a una base de datos. En lugar de dejar que A se conecte realmente, proporciona a A una interfaz que A puede usar para conectarse. Para probar, implementa esta interfaz con algunas cosas, sin conectarse, por supuesto. Si la clase B crea una instancia de A, debe pasar una conexión de base de datos "real" a A. Pero eso significa que B abre una conexión de base de datos. Eso significa que para probar B, se inyecta la conexión en B. Pero B se instancia en la clase C y así sucesivamente.

Entonces, ¿en qué punto debo decir "aquí obtengo datos de una base de datos y no escribiré una prueba unitaria para este fragmento de código"?

En otras palabras: en algún lugar del código en alguna clase debo llamar sqlDB.connect()o algo similar. ¿Cómo pruebo esta clase?

¿Y es lo mismo con el código que tiene que lidiar con una GUI o un sistema de archivos?


Quiero hacer Unit-Test. Cualquier otro tipo de prueba no está relacionado con mi pregunta. Sé que solo probaré una clase con ella (estoy muy de acuerdo contigo, Kilian). Ahora, alguna clase tiene que conectarse a un DB. Si quiero probar esta clase y preguntar "¿Cómo hago esto?", Muchos dicen: "¡Use la inyección de dependencia!" Pero eso solo cambia el problema a otra clase, ¿no? Entonces pregunto, ¿cómo pruebo la clase que realmente establece la conexión?

Pregunta extra: Algunas respuestas aquí se reducen a "¡Usa objetos simulados!" Qué significa eso? Me burlo de las clases de las que depende la clase bajo prueba. ¿Debo burlarme de la clase bajo prueba ahora y realmente probar la simulación (que se acerca a la idea de usar Métodos de plantilla, ver más abajo)?

TobiMcNamobi
fuente
¿Es la conexión de base de datos que está probando? ¿Sería aceptable crear una base de datos temporal en memoria (como derby )?
@MichaelT Todavía tengo que reemplazar el DB temporal en memoria con una base de datos real. ¿Dónde? ¿Cuando? ¿Cómo se prueba la unidad? ¿O está bien no probar la unidad de este código?
TobiMcNamobi
3
No hay nada para "prueba unitaria" sobre la base de datos. Lo mantienen otras personas, y si hubiera un error, debería dejar que lo arreglen en lugar de hacerlo usted mismo. Lo único que debería diferir entre el uso real de su clase y el uso durante las pruebas debería ser los parámetros de la conexión de la base de datos. Es poco probable que el código de lectura del archivo de propiedades, o el mecanismo de inyección Spring o lo que sea que use para entrelazar su aplicación esté roto (y si lo fuera, no podría arreglarlo usted mismo, vea más arriba), así que lo considero aceptable no para probar esta funcionalidad de fontanería.
Kilian Foth
2
@KilianFoth que está puramente relacionado con el entorno de trabajo y los roles de los empleados. Realmente no tiene nada que ver con la pregunta. ¿Qué pasa si no hay una persona responsable de la base de datos?
Reactgular
Algunos marcos de imitación le permiten inyectar objetos simulados en casi cualquier cosa, incluso en miembros privados y estáticos. Esto hace que las pruebas con cosas como conexiones db simuladas sean mucho más fáciles. Mockito + Powermock es lo que funciona para mí en estos días (son Java, no estoy seguro de en qué estás trabajando).
FrustratedWithFormsDesigner

Respuestas:

21

El objetivo de una prueba unitaria es probar una clase (de hecho, generalmente debería probar un método ).

Esto significa que cuando se prueba la clase A, se inyecta una base de datos de prueba en él - algo auto-escrito, o una velocidad del rayo base de datos en la memoria, lo que hace el trabajo.

Sin embargo, si prueba la clase B, de la cual es cliente A, generalmente se burla del Aobjeto completo con otra cosa, presumiblemente algo que hace su trabajo de una manera primitiva y preprogramada, sin usar un Aobjeto real y ciertamente sin usar datos. base (a menos que Apase toda la conexión de la base de datos a su interlocutor, pero eso es tan horrible que no quiero pensar en ello). Del mismo modo, cuando escribe una prueba unitaria para la clase C, de la cual es cliente B, se burlaría de algo que toma el papel By se olvidaría por Acompleto.

Si no lo hace, ya no es una prueba unitaria, sino una prueba de sistema o integración. Esos también son muy importantes, pero una caldera de peces completamente diferente. Para empezar, suelen ser más difíciles de configurar y ejecutar, no es factible exigir pasarlos como condición previa para los registros, etc.

Kilian Foth
fuente
11

Realizar pruebas unitarias contra una conexión de base de datos es perfectamente normal y una práctica común. Simplemente no es posible crear un puristenfoque donde todo en su sistema sea inyectable por dependencia.

La clave aquí es probar contra una base de datos temporal o de prueba solamente, y tener el proceso de inicio más ligero posible para construir esa base de datos de prueba.

Para pruebas unitarias en CakePHP hay cosas llamadas fixtures. Los accesorios son tablas de bases de datos temporales creadas sobre la marcha para una prueba unitaria. El dispositivo tiene métodos convenientes para crearlos. Pueden recrear un esquema de una base de datos de producción dentro de la base de datos de prueba, o puede definir el esquema usando una notación simple.

La clave del éxito con esto es no implementar la base de datos de negocios, sino enfocarse solo en el aspecto del código que está probando. Si tiene una prueba unitaria que verifica que un modelo de datos solo lee documentos publicados, entonces el esquema de la tabla para esa prueba solo debe tener los campos requeridos por ese código. No tiene que volver a implementar una base de datos de administración de contenido completa solo para probar ese código.

Algunas referencias adicionales.

http://en.wikipedia.org/wiki/Test_fixture

http://phpunit.de/manual/3.7/en/database.html

http://book.cakephp.org/2.0/en/development/testing.html#fixtures

Reactgular
fuente
28
Estoy en desacuerdo. Una prueba que requiere una conexión de base de datos no es una prueba unitaria, porque la prueba por su propia naturaleza tendrá efectos secundarios. Eso no significa que no pueda escribir una prueba automatizada, pero dicha prueba es, por definición, una prueba de integración, que ejercita áreas de su sistema más allá de su base de código.
KeithS
55
Llámame purista, pero mantengo el principio de que una prueba unitaria no debe realizar ninguna acción que abandone el "entorno limitado" del entorno de ejecución de prueba. No deben tocar bases de datos, sistemas de archivos, tomas de red, etc. Esto se debe a varias razones, entre las cuales la dependencia de la prueba depende del estado externo. Otro es el rendimiento; su conjunto de pruebas unitarias debería ejecutarse rápidamente, y la interfaz con estos datos externos almacena pruebas lentas por orden de magnitud. En mi propio desarrollo, uso simulacros parciales para probar cosas como mis repositorios, y me siento cómodo definiendo un "borde" para mi caja de arena.
KeithS
2
@gbjbaanb - Eso suena bien al principio, pero en mi experiencia es muy peligroso. Incluso en los conjuntos de pruebas y marcos mejor diseñados, el código para deshacer esta transacción puede no ejecutarse. Si el corredor de prueba falla o se interrumpe dentro de una prueba, o la prueba arroja un SOE u OOME, el mejor de los casos es que tenga una conexión y transacción colgadas en la base de datos que bloqueará las tablas que tocó hasta que se cortó la conexión. Las formas en que evita que esto cause problemas, como el uso de SQLite como base de datos de prueba, tienen sus propios inconvenientes, por ejemplo, el hecho de que realmente no está ejerciendo la base de datos real .
KeithS
55
@KeithS Creo que estamos debatiendo sobre la semántica. No se trata de cuál es la definición de una prueba unitaria o de una prueba de integración. Utilizo dispositivos para probar el código que depende de una conexión de base de datos. Si esa es una prueba de integración, entonces estoy bien con eso. Necesito saber que la prueba pasa. No me importaban las dependencias, el rendimiento o los riesgos. No sabré si ese código funciona a menos que la prueba pase. Para la mayoría de las pruebas no hay dependencias, pero para las que las hay, entonces esas dependencias no se pueden desacoplar. Es fácil decir que deberían ser, pero simplemente no pueden ser.
Reactgular
44
Creo que nosotros también. También uso un "marco de prueba de unidad" (NUnit) para mis pruebas de integración, pero me aseguro de segregar estas dos categorías de pruebas (a menudo en bibliotecas separadas). El punto que estaba tratando de hacer es que su conjunto de pruebas unitarias, el que ejecuta varias veces al día antes de cada check-in mientras sigue la metodología iterativa de refactorización rojo-verde, debe ser completamente aislable, para que pueda ejecutar estas pruebas varias veces al día sin pisar los dedos de sus compañeros de trabajo.
KeithS
4

Hay, en algún lugar de su base de código, una línea de código que realiza la acción real de conectarse a la base de datos remota. Esta línea de código es, 9 veces en 10, una llamada a un método "incorporado" proporcionado por las bibliotecas de tiempo de ejecución específicas para su idioma y entorno. Como tal, no es "su" código y, por lo tanto, no necesita probarlo; a los fines de una prueba unitaria, puede confiar en que esta llamada al método funcionará correctamente. Lo que puede y debe probar en su conjunto de pruebas unitarias son cosas como asegurarse de que los parámetros que se utilizarán para esta llamada sean los que espera que sean, como asegurarse de que la cadena de conexión sea correcta, o la instrucción SQL o nombre del procedimiento almacenado

Este es uno de los propósitos detrás de la restricción de que las pruebas unitarias no deben abandonar su "caja de arena" en tiempo de ejecución y depender del estado externo. En realidad es bastante práctico; El propósito de una prueba unitaria es verificar que el código que escribió (o está a punto de escribir, en TDD) se comporta de la manera que pensó que lo haría. El código que no escribió, como la biblioteca que está utilizando para realizar las operaciones de su base de datos, no debe ser parte del alcance de ninguna prueba unitaria, por la sencilla razón de que no lo escribió.

En su conjunto de pruebas de integración , estas restricciones son relajadas. Ahora puedesdiseñe pruebas que toquen la base de datos, para asegurarse de que el código que escribió se reproduzca bien con el código que no escribió. Sin embargo, estos dos conjuntos de pruebas deben permanecer separados porque su conjunto de pruebas unitarias es más efectivo cuanto más rápido se ejecute (por lo que puede verificar rápidamente que todas las afirmaciones hechas por los desarrolladores sobre su código aún se mantienen), y casi por definición, una prueba de integración es más lento en órdenes de magnitud debido a las dependencias agregadas en recursos externos. Deje que el robot de compilación se encargue de ejecutar su suite de integración completa cada pocas horas, ejecutando las pruebas que bloquean los recursos externos, para que los desarrolladores no se pisen los pies al ejecutar estas mismas pruebas localmente. Y si la construcción se rompe, ¿y qué? Se le da mucha más importancia a garantizar que el build-bot nunca falle una compilación de lo que probablemente debería ser.


Ahora, cuán estrictamente puede cumplir esto depende de su estrategia exacta para conectarse y consultar la base de datos. En muchos casos en los que debe usar el marco de acceso a datos "básico", como los objetos SqlConnection y SqlStatement de ADO.NET, un método completo desarrollado por usted puede consistir en llamadas a métodos integrados y otro código que depende de tener un conexión de base de datos, por lo que lo mejor que puede hacer en esta situación es burlarse de toda la función y confiar en sus conjuntos de pruebas de integración. También depende de qué tan dispuesto esté a diseñar sus clases para permitir que se reemplacen líneas de código específicas para fines de prueba (como la sugerencia de Tobi del patrón de Método de plantilla, que es bueno porque permite "simulacros parciales"

Si su modelo de persistencia de datos se basa en el código de su capa de datos (como desencadenantes, procesos almacenados, etc.), simplemente no hay otra forma de ejercer el código que usted mismo está escribiendo que desarrollar pruebas que vivan dentro de la capa de datos o crucen límite entre el tiempo de ejecución de su aplicación y el DBMS. Un purista diría que este patrón, por esta razón, debe evitarse en favor de algo como un ORM. No creo que vaya tan lejos; Incluso en la era de las consultas integradas en el lenguaje y otras operaciones de persistencia dependientes del dominio verificadas por el compilador, veo el valor de bloquear la base de datos solo para las operaciones expuestas a través del procedimiento almacenado, y por supuesto, dichos procedimientos almacenados deben verificarse utilizando pruebas Pero, tales pruebas no son pruebas unitarias . Son integracion pruebas

Si tiene un problema con esta distinción, generalmente se basa en una gran importancia otorgada a la "cobertura de código" completa, también conocida como "cobertura de prueba unitaria". Desea asegurarse de que cada línea de su código esté cubierta por una prueba unitaria. Un noble objetivo en su cara, pero digo tonterías; esa mentalidad se presta a antipatrones que se extienden mucho más allá de este caso específico, como escribir pruebas sin afirmaciones que se ejecutan pero no ejercentu codigo. Estos tipos de ejecuciones finales únicamente por el bien de los números de cobertura son más dañinos que relajar su cobertura mínima. Si desea asegurarse de que cada línea de su base de código se ejecute mediante alguna prueba automatizada, entonces eso es fácil; cuando calcule métricas de cobertura de código, incluya las pruebas de integración. Incluso podría ir un paso más allá y aislar estas disputadas pruebas "Itino" ("Integración solo de nombre"), y entre su conjunto de pruebas unitarias y esta subcategoría de pruebas de integración (que aún debería ejecutarse razonablemente rápido) debería obtener maldición casi cerca de la cobertura total.

KeithS
fuente
2

Las pruebas unitarias nunca deben conectarse a una base de datos. Por definición, deben probar una sola unidad de código cada uno (un método) en total aislamiento del resto de su sistema. Si no lo hacen, entonces no son una prueba unitaria.

Dejando a un lado la semántica, hay una gran cantidad de razones por las cuales esto es beneficioso:

  • Las pruebas ejecutan órdenes de magnitud más rápido
  • El bucle de retroalimentación se vuelve instantáneo (como ejemplo, retroalimentación <1s para TDD)
  • Las pruebas se pueden ejecutar en paralelo para construir / implementar sistemas
  • Las pruebas no necesitan una base de datos para ejecutarse (hace que la compilación sea mucho más fácil, o al menos más rápida)

Las pruebas unitarias son una forma de verificar su trabajo. Deben delinear todos los escenarios para un método dado, lo que generalmente significa todas las diferentes rutas a través de un método. Es su especificación que está construyendo, similar a la contabilidad de doble entrada.

Lo que está describiendo es otro tipo de prueba automatizada: una prueba de integración. Si bien también son muy importantes, idealmente tendrá mucho menos de ellos. Deben verificar que un grupo de unidades se integren entre sí correctamente.

Entonces, ¿cómo se prueban las cosas con el acceso a la base de datos? Todo su código de acceso a datos debe estar en una capa específica, por lo que el código de su aplicación puede interactuar con servicios simulables en lugar de la base de datos real. No debería importarle si esos servicios están respaldados por algún tipo de base de datos SQL, datos de prueba en memoria o incluso datos de servicios web remotos. Esa no es su preocupación.

Idealmente (y esto es muy subjetivo), desea que la mayor parte de su código esté cubierto por pruebas unitarias. Esto le da la confianza de que cada pieza funciona de forma independiente. Una vez que las piezas están construidas, debes unirlas. Ejemplo: cuando escribo la contraseña del usuario, debería obtener esta salida exacta.

Digamos que cada componente se compone de aproximadamente 5 clases: querría probar todos los puntos de falla dentro de ellos. Esto equivale a muchas menos pruebas solo para garantizar que todo esté conectado correctamente. Ejemplo: pruebe si puede encontrar al usuario de la base de datos con un nombre de usuario / contraseña.

Finalmente, desea algunas pruebas de aceptación para asegurarse de cumplir con los objetivos comerciales. Hay incluso menos de estos; pueden asegurarse de que la aplicación se esté ejecutando y haga lo que fue diseñada para hacer. Ejemplo: dados estos datos de prueba, debería poder iniciar sesión.

Piense en estos tres tipos de pruebas como una pirámide. Necesitas muchas pruebas unitarias para soportar todo, y luego avanzas desde allí.

Adrian Schneider
fuente
1

El patrón de método de plantilla podría ayudar.

Envuelve las llamadas a una base de datos en protectedmétodos. Para probar esta clase, realmente prueba un objeto falso que hereda de la clase de conexión de base de datos real y anula los métodos protegidos.

De esta forma, las llamadas reales a la base de datos nunca se someten a pruebas unitarias, es cierto. Pero son solo estas pocas líneas de código. Y eso es aceptable.

TobiMcNamobi
fuente
1
En caso de que se pregunte por qué respondo a mi propia pregunta: Sí, esta podría ser una respuesta, pero no estoy seguro de si es la correcta.
TobiMcNamobi
-1

Las pruebas con datos externos son pruebas de integración. Prueba de unidad significa que solo está probando la unidad. Se realiza principalmente con su lógica de negocios. Para que su unidad de código sea comprobable, debe seguir algunas pautas, como hacer que su unidad sea independiente de otra parte de su código. Durante la prueba unitaria, si necesita datos, debe inyectar esos datos con fuerza mediante inyección de dependencia. Hay un marco de burlas y tropezones por ahí.

DesarrolladorArnab
fuente