¿Cuál es la mejor estrategia para probar unidades de aplicaciones basadas en bases de datos?

Trabajo con muchas aplicaciones web impulsadas por bases de datos de complejidad variable en el back-end. Normalmente, hay una capa ORM separada de la lógica de presentación y de negocios. Esto hace que las pruebas unitarias sean bastante sencillas; las cosas se pueden implementar en módulos discretos y cualquier dato necesario para la prueba se puede falsificar a través de la burla del objeto.

Pero probar el ORM y la base de datos en sí siempre ha estado plagado de problemas y compromisos.

Con los años, he intentado algunas estrategias, ninguna de las cuales me satisfizo por completo.

  • Cargue una base de datos de prueba con datos conocidos. Ejecute pruebas contra el ORM y confirme que los datos correctos vuelven. La desventaja aquí es que su DB de prueba tiene que mantenerse al día con los cambios de esquema en la base de datos de la aplicación, y puede perder sincronía. También se basa en datos artificiales, y no puede exponer los errores que ocurren debido a la entrada del usuario estúpida. Finalmente, si la base de datos de prueba es pequeña, no revelará ineficiencias como un índice faltante. (OK, ese último no es realmente para lo que se deberían usar las pruebas unitarias, pero no duele).

  • Cargue una copia de la base de datos de producción y pruebe en contra de eso. El problema aquí es que puede que no tenga idea de lo que hay en el DB de producción en un momento dado; sus pruebas pueden necesitar ser reescritas si los datos cambian con el tiempo.

Algunas personas han señalado que ambas estrategias se basan en datos específicos, y una prueba unitaria debe probar solo la funcionalidad. Con ese fin, he visto sugerencias:

  • Utilice un servidor de base de prueba simulada y verifique solo que el ORM envíe las consultas correctas en respuesta a una llamada de método determinada.

¿Qué estrategias ha usado para probar aplicaciones basadas en bases de datos, si las hay? ¿Qué ha funcionado mejor para ti?

De hecho, he utilizado su primer enfoque con bastante éxito, pero de una manera ligeramente diferente que creo que resolvería algunos de sus problemas:

  1. Mantenga todo el esquema y las secuencias de comandos para crearlo en el control de código fuente para que cualquiera pueda crear el esquema de la base de datos actual después de una extracción. Además, mantenga los datos de muestra en los archivos de datos que se cargan por parte del proceso de comstackción. A medida que descubra los datos que causan errores, agréguelos a los datos de muestra para verificar que los errores no vuelvan a aparecer.

  2. Utilice un servidor de integración continua para comstackr el esquema de base de datos, cargar los datos de muestra y ejecutar pruebas. Así es como mantenemos nuestra base de datos de prueba sincronizada (reconstruyéndola en cada ejecución de prueba). Aunque esto requiere que el servidor CI tenga acceso y propiedad de su propia instancia de base de datos dedicada, digo que tener nuestro esquema db creado 3 veces al día ha ayudado a encontrar errores que probablemente no se hubieran encontrado hasta justo antes de la entrega (si no más tarde) ) No puedo decir que reconstruyo el esquema antes de cada compromiso. ¿Alguien? Con este enfoque no tendrás que (quizás deberíamos hacerlo, pero no es gran cosa si alguien se olvida).

  3. Para mi grupo, la entrada del usuario se realiza en el nivel de la aplicación (no en db), por lo que se prueba mediante pruebas unitarias estándar.

Cargando copia de la base de datos de producción:
Este fue el enfoque que se utilizó en mi último trabajo. Fue una gran causa de dolor de un par de problemas:

  1. La copia quedaría desactualizada a partir de la versión de producción
  2. Se realizarían cambios en el esquema de la copia y no se propagarían a los sistemas de producción. En este punto, tendríamos esquemas divergentes. No es divertido.

Servidor de base de datos burlona:
También hacemos esto en mi trabajo actual. Después de cada confirmación, ejecutamos pruebas unitarias contra el código de la aplicación que tiene inyectores falsos db inyectados. Luego, tres veces al día, ejecutamos la comstackción completa de db descrita anteriormente. Definitivamente recomiendo ambos enfoques.

Siempre estoy ejecutando pruebas contra una base de datos en memoria (HSQLDB o Derby) por estas razones:

  • Te hace pensar qué datos conservar en tu DB de prueba y por qué. Simplemente transportar su DB de producción a un sistema de prueba se traduce a “No tengo idea de lo que estoy haciendo o por qué, y si algo se rompe, ¡no fui yo!” 😉
  • Se asegura de que la base de datos se pueda recrear con poco esfuerzo en un lugar nuevo (por ejemplo, cuando tenemos que replicar un error de la producción)
  • Ayuda enormemente con la calidad de los archivos DDL.

El DB en memoria se carga con datos nuevos una vez que comienzan las pruebas y después de la mayoría de las pruebas, invoca ROLLBACK para mantenerlo estable. ¡SIEMPRE mantenga estables los datos en la base de datos de prueba! Si los datos cambian todo el tiempo, no puede realizar la prueba.

Los datos se cargan desde SQL, una plantilla DB o un volcado / copia de seguridad. Prefiero volcados si están en un formato legible porque puedo ponerlos en VCS. Si eso no funciona, utilizo un archivo CSV o XML. Si tengo que cargar enormes cantidades de datos … no los tengo. Nunca tiene que cargar enormes cantidades de datos 🙂 No para pruebas unitarias. Las pruebas de rendimiento son otro problema y se aplican reglas diferentes.

He estado haciendo esta pregunta por mucho tiempo, pero creo que no hay una solución mágica para eso.

Lo que hago actualmente es burlarme de los objetos DAO y mantener una representación en memoria de una buena colección de objetos que representan casos interesantes de datos que podrían vivir en la base de datos.

El principal problema que veo con ese enfoque es que solo está cubriendo el código que interactúa con su capa DAO, pero nunca prueba el DAO en sí, y en mi experiencia veo que también suceden muchos errores en esa capa. También guardo algunas pruebas unitarias que se ejecutan en la base de datos (por el uso de TDD o pruebas rápidas localmente), pero esas pruebas nunca se ejecutan en mi servidor de integración continua, ya que no mantenemos una base de datos para ese fin y yo Creo que las pruebas que se ejecutan en el servidor de CI deben ser autónomas.

Otro enfoque que me parece muy interesante, pero que no siempre vale la pena ya que consume un poco de tiempo, es crear el mismo esquema que utiliza para la producción en una base de datos incrustada que solo se ejecuta dentro de la unidad de prueba.

Aunque no cabe duda de que este enfoque mejora su cobertura, existen algunos inconvenientes, ya que debe estar lo más cerca posible de ANSI SQL para que funcione tanto con su DBMS actual como con el reemplazo integrado.

No importa lo que piense que sea más relevante para su código, hay algunos proyectos que pueden hacerlo más fácil, como DbUnit .

Incluso si hay herramientas que le permiten burlarse de su base de datos de una forma u otra (por ejemplo, MockConnection de MockConnection , que se puede ver en esta respuesta , descargo de responsabilidad, trabajo para el proveedor de jOOQ), aconsejaría no burlarse de las bases de datos más grandes con consultas complejas.

Incluso si solo desea probar la integración de su ORM, tenga en cuenta que un ORM emite una serie de consultas muy complejas a su base de datos, que pueden variar en

  • syntax
  • complejidad
  • orden (!)

Mocking todo eso para producir datos ficticios sensibles es bastante difícil, a menos que en realidad se está construyendo una pequeña base de datos dentro de su simulacro, que interpreta las instrucciones SQL transmitidas. Una vez dicho esto, utilice una conocida base de datos de prueba de integración que puede restablecer fácilmente con datos conocidos, contra los cuales puede ejecutar sus pruebas de integración.

Uso el primero (ejecutando el código en una base de datos de prueba). El único problema sustantivo que veo que planteas con este enfoque es la posibilidad de que los esquemas se desincronicen, lo que trato manteniendo un número de versión en mi base de datos y realizando todos los cambios de esquema a través de un script que aplica los cambios para cada incremento de versión.

También realizo todos los cambios (incluido el esquema de la base de datos) contra mi entorno de prueba primero, por lo que termina siendo al revés: después de que todas las pruebas pasen, aplique las actualizaciones de esquema al host de producción. También mantengo un par de pruebas separadas frente a bases de datos de aplicaciones en mi sistema de desarrollo para poder verificar allí que la actualización de db funciona correctamente antes de tocar la (s) caja (s) de producción real.

Para el proyecto basado en JDBC (directa o indirectamente, por ej., JPA, EJB, …) no puede simular toda la base de datos (en tal caso, sería mejor usar un db de prueba en un RDBMS real), sino solo una maqueta en el nivel JDBC .

La ventaja es la abstracción que viene con esa forma, ya que los datos JDBC (conjunto de resultados, recuento de actualizaciones, advertencia, …) son los mismos independientemente de su backend: su prod db, una prueba db o solo algunos datos de maquetas provistos para cada prueba caso.

Con la conexión JDBC simulada para cada caso, no es necesario administrar la prueba db (limpieza, solo una prueba al tiempo, accesorios de recarga, …). Cada conexión de maqueta está aislada y no hay necesidad de limpiarla. Solo se proporcionan accesorios mínimos necesarios en cada caso de prueba para simular el intercambio JDBC, lo que ayuda a evitar la complejidad de la gestión de una prueba completa db.

Acolyte framework incluye un controlador JDBC y utilidad para este tipo de maqueta: http://acolyte.eu.org .