Pregunta

Trabajo con muchas aplicaciones web que están basadas en bases de datos de complejidad variable en el backend. Por lo general, hay una capa de ORM separada de la lógica de presentación y de negocios. Esto hace que la prueba de la lógica empresarial sea bastante sencilla; las cosas se pueden implementar en módulos discretos y los datos necesarios para la prueba se pueden falsificar mediante el simulacro de los 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 por completo.

  • 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 que no exponga los 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, la última 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 ambas estrategias se basan en datos específicos, y que una prueba unitaria solo debe probar la funcionalidad. Para ello, he visto sugerido:

  • Use 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 las aplicaciones basadas en bases de datos, si las hay? ¿Qué ha funcionado mejor para ti?

¿Fue útil?

Solución

En realidad, utilicé tu primer enfoque con bastante éxito, pero de una forma ligeramente diferente, creo que resolvería algunos de tus problemas:

  1. 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.

  2. Use 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 de 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).

  3. 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 debido a un par de problemas:

  1. La copia quedaría desactualizada de la versión de producción
  2. 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.

Servidor de base de datos de burla:
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.

Otros consejos

Siempre estoy ejecutando pruebas en una base de datos en memoria (HSQLDB o Derby) por estos motivos:

  • 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 en "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 la base de datos de prueba estable! Si los datos cambian todo el tiempo, no se puede realizar la prueba.

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 problema y se aplican reglas diferentes.

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 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 ocurren muchos errores en esa capa como bien. También mantengo algunas pruebas unitarias que se ejecutan contra la base de datos (por el uso de TDD o pruebas rápidas a nivel local), 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 yo Creo que las pruebas que se ejecutan en el servidor CI deberían ser autocontenidas.

Otro enfoque que encuentro muy interesante, pero que no siempre vale la pena, ya que requiere un poco de tiempo, es crear el mismo esquema que se utiliza 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 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 crea que es más relevante para su código, hay algunos proyectos que lo pueden hacer más fácil, como DbUnit .

Incluso si hay herramientas que le permiten simular su base de datos de una forma u otra (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 simular 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

  • sintaxis
  • complejidad
  • orden (!)

Burlarse de todo eso para producir datos ficticios sensibles es bastante difícil, a menos que en realidad estés construyendo una pequeña base de datos dentro de tu 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.

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 todos los cambios (incluido el esquema de la base de datos) en mi entorno de prueba primero, por lo que termina siendo al revés: después de que pasen todas las pruebas, aplique las actualizaciones del esquema al host de producción. También mantengo un par separado de bases de datos de prueba y aplicación en mi sistema de desarrollo para que pueda verificar que la actualización de db funciona correctamente antes de tocar la (s) caja (s) de producción real.

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

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 propósito principal es hacer que los datos utilizados por la prueba

  1. muy cerca de la prueba
  2. explícito (usar archivos SQL para datos hace que sea muy problemático ver qué datos se usan en qué prueba)
  3. aísle 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 prueba y omitir cosas irrelevantes.

Para dar una idea de lo que significa en la práctica, considere la prueba para algún DAO que funciona con Comment s a Post s escrito por Authors . Para probar las operaciones 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:

  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 constructor y / o la fábrica)
  2. 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 vs Commit

Me parece más conveniente que las pruebas se confirmen cuando se ejecutan. En primer lugar, algunos efectos (por ejemplo, RESTRICCIONES DEFENDIDAS ) 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 con él, por lo que las colisiones son poco frecuentes. 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).

Para proyectos basados ??en JDBC (directa o indirectamente, p. ej., JPA, EJB, ...) no puede utilizar la base de datos completa (en ese caso, sería mejor utilizar una base de datos de prueba en un RDBMS real), pero solo simulación a nivel JDBC.

La ventaja es la abstracción que viene con esa forma, ya que los datos de 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 maqueta proporcionados para cada caso de prueba.

Con la conexión JDBC simulada para cada caso, no hay necesidad de 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 es mi marco que incluye un controlador JDBC y una utilidad para este tipo de maqueta: http://acolyte.eu. org .

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top