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

346

Trabajo con muchas aplicaciones web que funcionan con bases de datos de diversa complejidad en el back-end. Por lo general, hay una capa ORM separada de la lógica empresarial y de presentación. Esto hace que las pruebas unitarias de la lógica comercial sean bastante sencillas; las cosas se pueden implementar en módulos discretos y cualquier información necesaria para la prueba se puede falsificar mediante la burla de objetos.

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 vuelven los datos correctos. La desventaja aquí es que su base de datos de prueba tiene que mantenerse al día con cualquier cambio de esquema en la base de datos de la aplicación, y podría no estar sincronizado. También se basa en datos artificiales, y no puede exponer errores que ocurren debido a la entrada estúpida del usuario. Finalmente, si la base de datos de prueba es pequeña, no revelará ineficiencias como un índice faltante. (Bien, ese último no es realmente para qué se deberían usar las pruebas unitarias, pero no duele).

  • Cargue una copia de la base de datos de producción y pruebe con eso. El problema aquí es que es posible que no tenga idea de lo que hay en la base de datos de producción en un momento dado; Es posible que sea necesario reescribir sus pruebas 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 debería probar solo la funcionalidad. Con ese fin, he visto sugerido:

  • Use un servidor de base de datos simulado y verifique solo que el ORM esté enviando las consultas correctas en respuesta a una llamada de método dada.

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

friedo
fuente
Creo que aún debería tener índices de base de datos en un entorno de prueba para casos como índices únicos.
dtc
Personalmente, no me importa esta pregunta aquí, pero si seguimos las reglas, esta pregunta no es para stackoverflow sino para el sitio web softwareengineering.stackexchange .
ITExpert

Respuestas:

155

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 origen para que cualquiera pueda crear el esquema de la base de datos actual después de una comprobación. Además, mantenga datos de muestra en archivos de datos que se cargan por parte del proceso de compilación. A medida que descubra datos que causan errores, agréguelos a sus datos de muestra para verificar que los errores no vuelvan a aparecer.

  2. Utilice un servidor de integración continua para crear el esquema de la base de datos, cargar los datos de muestra y ejecutar pruebas. Así es como mantenemos sincronizada nuestra base de datos de prueba (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 construido 3 veces al día ha ayudado dramáticamente a encontrar errores que probablemente no se habrían encontrado hasta justo antes de la entrega (si no más tarde ) No puedo decir que reconstruyo el esquema antes de cada confirmación. ¿Alguien? Con este enfoque no tendrá que hacerlo (bueno, tal vez deberíamos, pero no es gran cosa si alguien se olvida).

  3. Para mi grupo, la entrada del usuario se realiza a nivel de aplicación (no db), por lo que esto se prueba a través de pruebas unitarias estándar.

Carga de la copia de la base de datos de producción:
este fue el enfoque que utilicé en mi último trabajo. Fue una gran causa de un par de problemas:

  1. La copia quedaría desactualizada 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 burlón:
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 db simulados inyectados. Luego, tres veces al día, ejecutamos la compilación db completa descrita anteriormente. Definitivamente recomiendo ambos enfoques.

Mark Roddy
fuente
37
Cargar una copia de la base de datos de producción también tiene implicaciones de seguridad y privacidad. Una vez que se hace grande, tomar una copia y ponerlo en su entorno de desarrollo puede ser un gran problema.
WW.
Sinceramente, esto es un gran dolor. Soy nuevo en las pruebas y también escribí un orm que quiero probar. Ya utilicé tu primer método, pero leí que no es la unidad de prueba. Utilizo la funcionalidad específica del motor db y burlarme de un DAO será difícil. Creo que usaré mi método actual, ya que funciona y otros lo usan. Pruebas automatizadas de rock por cierto. Gracias.
frostymarvelous
2
Administro dos grandes proyectos diferentes, en uno de ellos este enfoque fue perfecto, pero hemos tenido muchos problemas para intentar implementar esto en el otro proyecto. Así que creo que eso depende de cuán fácilmente se pueda recrear el esquema cada vez que se ejecuten las pruebas, actualmente estoy trabajando para encontrar una nueva solución para este último problema.
Cruz
2
En este caso, definitivamente vale la pena usar una herramienta de control de versiones de bases de datos como Roundhouse, algo que puede ejecutar migraciones. Esto se puede ejecutar en cualquier instancia de base de datos y debe asegurarse de que los esquemas estén actualizados. Además, cuando se escriben los scripts de migración, los datos de prueba también deben escribirse, manteniendo las migraciones y los datos sincronizados.
jedd.ahyoung
mejor usar parches y burlas de mono y evitar operaciones de escritura
Nickpick
56

Siempre estoy ejecutando pruebas contra un DB en memoria (HSQLDB o Derby) por estos motivos:

  • Te hace pensar qué datos guardar en tu base de datos de prueba y por qué. Simplemente transportar su base de datos de producción a un sistema de prueba se traduce como "¡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 volver a crear con poco esfuerzo en un lugar nuevo (por ejemplo, cuando necesitamos replicar un error de 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, invoco 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 probar.

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

Aaron Digulla
fuente
1
¿Es la velocidad la única razón para usar (específicamente) un DB en memoria?
rinogo
2
Supongo que otra ventaja podría ser su naturaleza "desechable": no es necesario limpiar después de ti; solo mata el DB en memoria. (Pero hay otras formas de lograr esto, como el enfoque ROLLBACK que ha mencionado)
rinogo
1
La ventaja es que cada prueba puede elegir su estrategia individualmente. Tenemos pruebas que hacen el trabajo en subprocesos secundarios, lo que significa que Spring siempre confirmará los datos.
Aaron Digulla
@ Aaron: también estamos siguiendo esta estrategia. Me gustaría saber cuál es su estrategia para afirmar que el modelo en memoria tiene la misma estructura que el db real.
Guillaume
1
@Guillaume: estoy creando todas las bases de datos a partir de los mismos archivos SQL. H2 es excelente para esto, ya que admite la mayoría de las idiosincrasias SQL de las principales bases de datos. Si eso no funciona, entonces uso un filtro que toma el SQL original y lo convierte en el SQL para la base de datos en memoria.
Aaron Digulla
14

He estado haciendo esta pregunta durante mucho tiempo, pero creo que no hay una bala de plata para eso.

Lo que actualmente hago 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 cubre 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 ocurren muchos errores en esa capa. También mantengo 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 tenemos una base de datos para ese propósito y piensa que las pruebas que se ejecutan en el servidor CI deben ser independientes.

Otro enfoque que encuentro muy interesante, pero que no siempre vale la pena, ya que lleva un poco de tiempo, es crear el mismo esquema que usa para la producción en una base de datos integrada que solo se ejecuta dentro de las pruebas unitarias.

Aunque no hay 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 incorporado.

No importa lo que considere más relevante para su código, existen algunos proyectos que pueden facilitarlo, como DbUnit .

kolrie
fuente
13

Incluso si hay herramientas que le permiten burlas de su base de datos de un modo u otro (por ejemplo jOOQ 's MockConnection, que se puede ver en esta respuesta - descargo de responsabilidad, trabajo para el proveedor de jOOQ), aconsejaría no para burlarse de las bases de datos más grandes con complejo consultas

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

  • sintaxis
  • complejidad
  • orden (!)

Es bastante difícil burlarse de todo eso para producir datos ficticios sensibles, a menos que realmente esté construyendo una pequeña base de datos dentro de su simulación, que interpreta las declaraciones SQL transmitidas. Dicho esto, use una base de datos de prueba de integración conocida que pueda restablecer fácilmente con datos conocidos, contra los cuales puede ejecutar sus pruebas de integración.

Lukas Eder
fuente
5

Yo uso el primero (ejecutar el código contra una base de datos de prueba). El único problema sustantivo que veo surgir 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 haciendo todos los cambios de esquema a través de un script que aplica los cambios para cada incremento de versión.

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

Dave Sherohman
fuente
3

Estoy usando el primer enfoque, pero un poco diferente que permite abordar los problemas que mencionó.

Todo lo que se necesita para ejecutar pruebas para DAO está en el control de origen. Incluye esquema y scripts para crear la base de datos (docker es muy bueno para esto). Si se puede usar el DB incrustado, lo uso para la velocidad.

La diferencia importante con los otros enfoques descritos es que los datos que se requieren para la prueba no se cargan desde scripts SQL o archivos XML. Todo (excepto algunos datos del diccionario que son efectivamente constantes) es creado por la aplicación usando funciones / clases de utilidad.

El objetivo principal es hacer que los datos sean utilizados por la prueba.

  1. muy cerca de la prueba
  2. explícito (el uso de archivos SQL para datos hace que sea muy problemático ver qué datos se utilizan para qué prueba)
  3. aislar las pruebas de los cambios no relacionados.

Básicamente significa que estas utilidades permiten especificar declarativamente solo cosas esenciales para la prueba en la prueba misma y omitir cosas irrelevantes.

Para dar una idea de lo que significa en la práctica, considere la prueba para algunos DAO que funcionan con Comments to Posts escritos por Authors. Para probar las operaciones CRUD para tal DAO, se deben crear algunos datos en la base de datos. La prueba se vería así:

@Test
public void savedCommentCanBeRead() {
    // Builder is needed to declaratively specify the entity with all attributes relevant
    // for this specific test
    // Missing attributes are generated with reasonable values
    // factory's responsibility is to create entity (and all entities required by it
    //  in our example Author) in the DB
    Post post = factory.create(PostBuilder.post());

    Comment comment = CommentBuilder.comment().forPost(post).build();

    sut.save(comment);

    Comment savedComment = sut.get(comment.getId());

    // this checks fields that are directly stored
    assertThat(saveComment, fieldwiseEqualTo(comment));
    // if there are some fields that are generated during save check them separately
    assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));        
}

Esto tiene varias ventajas sobre los scripts SQL o los archivos XML con datos de prueba:

  1. Mantener el código es mucho más fácil (agregar una columna obligatoria, por ejemplo, en alguna entidad a la que se hace referencia en muchas pruebas, como Autor, no requiere cambiar muchos archivos / registros, sino solo un cambio en el generador y / o la fábrica)
  2. Los datos requeridos por una prueba específica se describen en la prueba misma y no en algún otro archivo. Esta proximidad es muy importante para la comprensión de la prueba.

Rollback vs Commit

Me parece más conveniente que las pruebas se confirmen cuando se ejecutan. En primer lugar, algunos efectos (por ejemploDEFERRED CONSTRAINTS ) no se pueden comprobar si nunca se realiza la confirmación. En segundo lugar, cuando falla una prueba, los datos se pueden examinar en la base de datos, ya que no se revierte la reversión.

De hecho, esto tiene el inconveniente de que la prueba puede producir datos rotos y esto conducirá a fallas en otras pruebas. Para lidiar con esto trato de aislar las pruebas. En el ejemplo anterior, cada prueba puede crear nuevas Authory todas las demás entidades se crean relacionadas con ella, por lo que las colisiones son raras. Para tratar con los invariantes restantes que pueden romperse potencialmente pero no pueden expresarse como una restricción de nivel de DB, utilizo algunas comprobaciones programáticas para condiciones erróneas que pueden ejecutarse después de cada prueba individual (y se ejecutan en CI pero generalmente se desconectan localmente para el rendimiento razones).

Konoval romano
fuente
Si inicia la base de datos utilizando entidades y el orm en lugar de scripts SQL, también tiene la ventaja de que el compilador lo obligará a corregir el código inicial si realiza cambios en su modelo. Solo es relevante si utiliza un lenguaje estático escrito, por supuesto.
daramasala
Entonces, para aclarar: ¿está utilizando las funciones / clases de utilidad en toda su aplicación o solo para sus pruebas?
Ella
@Ella, estas funciones de utilidad generalmente no son necesarias fuera del código de prueba. Piensa por ejemplo en PostBuilder.post(). Genera algunos valores para todos los atributos obligatorios de la publicación. Esto no es necesario en el código de producción.
Roman Konoval
2

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

La ventaja es la abstracción que viene de esa manera, ya que los datos JDBC (conjunto de resultados, recuento de actualizaciones, advertencia, ...) son los mismos, sea cual sea el backend: su prod db, un db de prueba o solo algunos datos de maquetas proporcionados para cada prueba caso.

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

Acolyte es mi marco que incluye un controlador JDBC y una utilidad para este tipo de maquetas: http://acolyte.eu.org .

cchantep
fuente