database - ¿Cuál es la mejor estrategia para probar las unidades de base de datos de aplicaciones?
unit-testing orm (7)
De hecho, he utilizado su primer enfoque con bastante éxito, pero creo que de una forma ligeramente diferente resolvería algunos de sus problemas:
Mantenga el esquema completo y las secuencias de comandos para crearlo en el control de código fuente para que cualquiera pueda crear el esquema de 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 construcció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.
Utilice un servidor de integración continua para generar el esquema de la 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 de CI tenga acceso y sea propietario de su propia instancia de base de datos dedicada, digo que tener nuestro esquema db construido 3 veces al día ha ayudado enormemente 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 confirmación. ¿Alguien? Con este enfoque no tendrá que hacerlo (bueno, deberíamos, pero no es un gran problema si alguien se olvida).
Para mi grupo, la entrada del usuario se realiza en el nivel de la aplicación (no en la base de datos), por lo que se prueba mediante pruebas unitarias estándar.
Cargando copia de base de datos de producción:
Este fue el enfoque que se utilizó en mi último trabajo. Fue un gran dolor por un par de problemas:
- La copia quedaría desactualizada de la versión de producción.
- Los cambios se realizarían 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.
Mocking Database Server:
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 inyectados simuladores de acceso a base de datos. Luego, tres veces al día ejecutamos la compilación de db completa descrita anteriormente. Definitivamente recomiendo ambos enfoques.
Trabajo con una gran cantidad de aplicaciones web que son impulsadas por bases de datos de complejidad variable en el backend. Normalmente, hay una capa ORM separada de la lógica de presentación y negocio. Esto hace que la prueba de la lógica de negocios sea bastante sencilla; las cosas se pueden implementar en módulos discretos y cualquier dato necesario para la prueba se puede falsificar a través del simulacro de objetos.
Pero probar el ORM y la base de datos en sí siempre ha estado lleno de problemas y compromisos.
A lo largo de los años, he probado algunas estrategias, ninguna de las cuales me ha satisfecho completamente.
Cargar una base de datos de prueba con datos conocidos. Ejecute pruebas contra el ORM y confirme que los datos correctos regresan. La desventaja aquí es que su base de datos de prueba debe mantenerse al día con cualquier cambio de esquema en la base de datos de la aplicación, y es posible que no esté sincronizado. También se basa en datos artificiales, y puede no exponer errores que se producen debido a la entrada de usuarios estúpidos. Finalmente, si la base de datos de prueba es pequeña, no revelará ineficiencias como un índice faltante. (De acuerdo, este último no es realmente para lo que se debe usar la prueba de unidad, pero no duele).
Cargue una copia de la base de datos de producción y pruebe con 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; Es posible que deba reescribir sus pruebas si los datos cambian con el tiempo.
Algunas personas han señalado que estas dos estrategias se basan en datos específicos, y que una prueba unitaria solo debe probar la funcionalidad. Para ello, he visto sugerido:
- Utilice un servidor de base de datos simulado y compruebe 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?
Estoy utilizando el primer enfoque, pero un poco diferente, que permite abordar los problemas que mencionó.
Todo lo que se necesita para ejecutar pruebas para los DAO está en el control de origen. Incluye esquema y scripts para crear la base de datos (la ventana acoplable es muy buena para esto). Si se puede usar el DB incorporado, 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 es efectivamente constante) es creado por la aplicación usando funciones / clases de utilidad.
El objetivo principal es hacer datos utilizados por prueba.
- muy cerca de la prueba
- explícito (el uso de archivos SQL para datos hace que sea muy problemático ver qué datos se utilizan en qué prueba)
- Aislar las pruebas de los cambios no relacionados.
Básicamente significa que estas utilidades permiten especificar de manera declarativa solo las cosas esenciales para la prueba en sí y omitir cosas irrelevantes.
Para dar una idea de lo que significa en la práctica, considere la prueba para algunos DAO que funciona con los Comment
a las Post
escritos por Authors
. Para probar las operaciones de CRUD para dicho DAO, se deben crear algunos datos en la base de datos. La prueba se vería como:
@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 archivos XML con datos de prueba:
- 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 constructor y / o en la fábrica)
- Los datos requeridos por una prueba específica se describen en la prueba en sí y no en algún otro archivo. Esta proximidad es muy importante para la comprensibilidad de la prueba.
Rollback o no Rollback
Me parece más conveniente que las pruebas se confirmen cuando se ejecutan. En primer lugar, algunos efectos (por ejemplo, DEFERRED CONSTRAINTS
) no pueden verificarse si la confirmación nunca ocurre. En segundo lugar, cuando una prueba falla, los datos se pueden examinar en la base de datos, ya que no se revierten con la reversión.
Debido a que esto tiene un inconveniente, la prueba puede producir datos rotos y esto llevará a fallas en otras pruebas. Para lidiar con esto trato de aislar las pruebas. En el ejemplo anterior, cada prueba puede crear un nuevo Author
y todas las demás entidades se crean relacionadas, por lo que las colisiones son raras. Para lidiar con los invariantes restantes que pueden romperse potencialmente pero no pueden expresarse como una restricción de nivel de DB, utilizo algunas verificaciones programáticas para condiciones erróneas que pueden ejecutarse después de cada prueba (y se ejecutan en CI, pero generalmente están desactivadas localmente para el rendimiento razones).
He estado haciendo esta pregunta durante mucho tiempo, pero creo que no hay una bala de plata para eso.
Lo que actualmente hago es burlarse de los objetos DAO y mantener una representación en la memoria de una buena colección de objetos que representan casos interesantes de datos que podrían estar en la base de datos.
El principal problema que veo con este enfoque es que está cubriendo solo el código que interactúa con su capa DAO, pero nunca está probando el propio DAO, y en mi experiencia veo que también ocurren muchos errores en esa capa. También mantengo algunas pruebas unitarias que se ejecutan contra 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 propósito y Las pruebas de pensar que se ejecutan en el servidor de CI deben ser autónomas.
Otro enfoque que encuentro muy interesante, pero que no siempre vale la pena, ya que consume poco tiempo, es crear el mismo esquema que se usa para la producción en una base de datos integrada que solo se ejecuta dentro de la prueba de la unidad.
Aunque no hay duda de que este enfoque mejora su cobertura, hay algunos inconvenientes, ya que tiene que estar lo más cerca posible de ANSI SQL para que funcione con su DBMS actual y el reemplazo integrado.
No importa lo que crea que es más relevante para su código, hay algunos proyectos que lo pueden hacer más fácil, como DbUnit .
Para el proyecto basado en JDBC (directa o indirectamente, por ejemplo, JPA, EJB, ...) no puede hacer una maqueta de toda la base de datos (en ese caso sería mejor usar una base de datos de prueba en un RDBMS real), pero 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 lo que sea que sea el backend: su base de datos de prod, una base de datos de prueba, o simplemente algunos datos de simulación proporcionados para cada prueba caso.
Con la conexión JDBC simulada para cada caso, no es necesario administrar la base de datos de prueba (limpieza, solo una prueba a la vez, recargar los dispositivos, ...). Cada conexión de maqueta está aislada y no hay necesidad de limpiar. En cada caso de prueba solo se proporcionan dispositivos mínimos necesarios para simular el intercambio de JDBC, lo que ayuda a evitar la complejidad de administrar una base de datos de prueba completa.
Acolyte Framework incluye un controlador JDBC y una utilidad para este tipo de maqueta: http://acolyte.eu.org .
Siempre estoy ejecutando pruebas en una base de datos en memoria (HSQLDB o Derby) por las siguientes razones:
- Te hace pensar qué datos mantener en tu base de datos de prueba y por qué. El solo hecho de 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 recrear 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.
La base de datos 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 mantenerla estable. ¡SIEMPRE mantenga los datos en el DB de prueba estable! Si los datos cambian todo el tiempo, no se puede probar.
Los datos se cargan desde SQL, una plantilla DB 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 tema y se aplican diferentes reglas.
Uso el primero (ejecutando el código en una base de datos de prueba). El único problema importante que veo que plantea con este enfoque es la posibilidad de que los esquemas se desincronicen, lo cual trato al mantener un número de versión en mi base de datos y hacer todos los cambios de esquema a través de un script que aplica los cambios para cada incremento de versión.
También hago 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 todas las pruebas pasen, aplique las actualizaciones del esquema al host de producción. También mantengo un par separado de bases de datos de prueba y de aplicación en mi sistema de desarrollo para poder verificar que la actualización de db funciona correctamente antes de tocar la (s) caja (s) de producción real.
Incluso si hay herramientas que le permiten simular 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), le aconsejo que no se burle de bases de datos más grandes con Consultas complejas.
Incluso si solo desea realizar una prueba de 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
- sintaxis
- complejidad
- orden (!)
La burla de 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 sentencias de SQL transmitidas. Dicho esto, utilice una base de datos de pruebas de integración conocida que pueda restablecer fácilmente con datos conocidos, con la que pueda ejecutar sus pruebas de integración.