unit testing - tutorial - Datos de base de datos necesarios en las pruebas de integración; creado por llamadas API o utilizando datos importados?
pruebas unitarias codeigniter (8)
Esta pregunta es más o menos un lenguaje de programación agnóstico. Sin embargo, como estoy principalmente en Java estos días, de allí sacaré mis ejemplos. También estoy pensando en el caso OOP, así que si quieres probar un método necesitas una instancia de esa clase de métodos.
Una regla básica para las pruebas unitarias es que deben ser autónomas, y eso se puede lograr aislando una clase de sus dependencias. Hay varias formas de hacerlo y depende de si inyectas tus dependencias usando IoC (en el mundo de Java tenemos Spring, EJB3 y otros frameworks / plataformas que proporcionan capacidades de inyección) y / o si te burlas de los objetos (para Java tienes JMock y EasyMock ) para separar una clase que se está probando de sus dependencias.
Si necesitamos probar grupos de métodos en diferentes clases * y ver que están bien integrados, escribimos pruebas de integración . ¡Y aquí está mi pregunta!
- Al menos en las aplicaciones web, el estado a menudo persiste en una base de datos. Podríamos usar las mismas herramientas que para las pruebas unitarias para lograr la independencia de la base de datos. Pero en mi humilde opinión, creo que hay casos en los que no usar una base de datos para pruebas de integración es burlarse demasiado (pero no dude en estar en desacuerdo, no utilizar una base de datos, es una respuesta válida ya que hace que la pregunta sea irrelevante )
- Cuando usa una base de datos para pruebas de integración, ¿cómo llena esa base de datos con datos? Puedo ver dos enfoques:
- Almacene los contenidos de la base de datos para la prueba de integración y cárguela antes de comenzar la prueba. Si se almacena como un volcado de SQL, un archivo de base de datos, XML u otra cosa sería interesante saber.
- Cree las estructuras de base de datos necesarias mediante llamadas API. Estas llamadas probablemente se dividen en varios métodos en su código de prueba y cada uno de estos métodos puede fallar. Podría ser visto como su prueba de integración que tiene dependencias en otras pruebas.
¿Cómo se asegura de que los datos de la base de datos necesarios para las pruebas estén allí cuando los necesite? ¿Y por qué elegiste el método que eliges?
Por favor, proporcione una respuesta con una motivación , ya que está motivada por la parte interesante. Recuerda eso solo diciendo "¡Es la mejor práctica!" no es una motivación real , solo es repetir algo que has leído o escuchado de alguien. Para ese caso, explique por qué es la mejor práctica.
* Incluyo un método para llamar a otros métodos en (la misma u otra) instancia de la misma clase en mi definición de prueba unitaria, aunque técnicamente podría no ser correcta. No dude en corregirme, pero mantengamos esto como un problema secundario.
En las pruebas de integración, debe probar con una base de datos real, ya que debe verificar que su aplicación realmente pueda comunicarse con la base de datos. Aislar la base de datos como dependencia significa que está posponiendo la prueba real de si su base de datos se implementó correctamente, su esquema es el esperado y su aplicación está configurada con la cadena de conexión correcta. No querrá encontrar ningún problema con esto cuando se despliegue en producción.
También desea probar con los conjuntos de datos previamente creados y el conjunto de datos vacío. Debe probar tanto la ruta donde su aplicación comienza con una base de datos vacía con solo sus datos iniciales predeterminados y comienza a crear y completar los datos y también con un conjunto de datos bien definidos que se dirigen a las condiciones específicas que desea probar, como el estrés, el rendimiento y pronto.
Además, asegúrese de tener la base de datos en un estado conocido antes de cada estado. No desea tener dependencias entre sus pruebas de integración.
Es probable que esto no responda a todas sus preguntas, en caso de haberlas, pero tomé la decisión en un proyecto de realizar pruebas unitarias contra el DB. En mi caso, sentí que la estructura de DB también necesitaba pruebas, es decir, mi diseño de base de datos proporcionaba lo que necesitaba la aplicación. Más adelante en el proyecto, cuando sienta que la estructura de DB es estable, probablemente me alejaré de esto.
Para generar datos, decidí crear una aplicación externa que llenó el DB con datos "aleatorios". Creé un generador de nombre de persona y nombre de compañía, etc.
La razón para hacer esto en un programa externo era: 1. Podría volver a ejecutar las pruebas con los datos modificados de la prueba, es decir, asegurarme de que mis pruebas podían ejecutarse varias veces y que la modificación de los datos realizada por las pruebas eran modificaciones válidas. 2. Si fuera necesario, podría limpiar el DB y comenzar de nuevo.
Estoy de acuerdo en que hay puntos de falla en este enfoque, pero en mi caso ya que, por ejemplo, la generación de personas era parte de la lógica de negocios, la generación de datos para las pruebas también estaba probando esa parte.
Generalmente uso scripts SQL para completar los datos en el escenario que discute.
Es directo y muy fácilmente repetible.
Hago ambas cosas, según lo que necesite probar:
Importe datos de prueba estáticos de scripts SQL o volcados de bases de datos. Estos datos se usan en la carga de objetos (deserialización o mapeo de objetos) y en las pruebas de consulta SQL (cuando quiero saber si el código arrojará el resultado correcto).
Además, suelo tener algunos datos de red troncal (configuración, valor para nombrar tablas de búsqueda, etc.). Estos también se cargan en este paso. Tenga en cuenta que esta carga es una prueba única (junto con la creación de la base de datos desde cero).
Cuando tengo un código que modifica el DB (objeto -> DB), generalmente lo ejecuto contra un DB vivo (en memoria o una instancia de prueba en alguna parte). Esto es para asegurar que el código funcione; no crear una gran cantidad de filas Después de la prueba, retrotrajo la transacción (siguiendo la regla de que las pruebas no deben modificar el estado global).
Por supuesto, hay excepciones a la regla:
- También creo una gran cantidad de filas en las pruebas de rendimiento.
- A veces, tengo que confirmar el resultado de una prueba unitaria (de lo contrario, la prueba aumentaría demasiado).
Parece que tu pregunta es en realidad dos preguntas. ¿Debería usar excluir la base de datos de sus pruebas? Cuando tienes una base de datos, ¿cómo debes generar los datos en la base de datos?
Cuando sea posible, prefiero usar una base de datos real. Con frecuencia, las consultas (escritas en SQL, HQL, etc.) en las clases CRUD pueden arrojar resultados sorprendentes cuando se confrontan con una base de datos real. Es mejor resolver estos problemas desde el principio. A menudo, los desarrolladores escribirán pruebas unitarias muy delgadas para CRUD; probando solo los casos más benignos. El uso de una base de datos real para su prueba puede probar todos los casos de esquinas de esquina que puede que ni siquiera haya tenido en cuenta.
Dicho esto, puede haber otros problemas. Después de cada prueba, desea devolver su base de datos a un estado conocido. En mi trabajo actual nukeamos la base de datos al ejecutar todas las declaraciones DROP y luego recrear completamente todas las tablas desde cero. Esto es extremadamente lento en Oracle, pero puede ser muy rápido si usa una base de datos en memoria como HSQLDB. Cuando necesitamos solucionar problemas específicos de Oracle, simplemente cambiamos la URL de la base de datos y las propiedades del controlador y luego corremos contra Oracle. Si no tiene este tipo de portabilidad de base de datos, Oracle también tiene algún tipo de característica de instantánea de base de datos que puede usarse específicamente para este propósito; retrotraer la base de datos completa a algún estado anterior. Estoy seguro de lo que otras bases de datos tienen.
Dependiendo de qué tipo de datos estará en su base de datos, la API o el enfoque de carga pueden funcionar mejor o peor. Cuando tienes datos altamente estructurados con muchas relaciones, las API te facilitarán la vida al hacer explícitas las relaciones entre tus datos. Será más difícil para usted cometer un error al crear su conjunto de datos de prueba. Como se menciona en otros carteles, las herramientas de refactorización pueden encargarse de algunos de los cambios en la estructura de sus datos de forma automática. A menudo me resulta útil pensar en los datos de prueba generados por API como componiendo un escenario; cuando un usuario / sistema ha realizado los pasos X, YZ y luego las pruebas irán desde allí. Estos estados se pueden lograr porque puede escribir un programa que llame a la misma API que su usuario usaría.
La carga de datos se vuelve mucho más importante cuando necesita grandes volúmenes de datos, tiene pocas relaciones entre sus datos o hay consistencia en los datos que no pueden expresarse mediante API o mecanismos relacionales estándar. En un trabajo que trabajó en mi equipo, estaba escribiendo la aplicación de informes para un gran sistema de inspección de paquetes de red. El volumen de datos fue bastante grande por el momento. Para activar un subconjunto útil de casos de prueba, realmente necesitábamos datos de prueba generados por los sniffers. De esta forma, las correlaciones entre la información sobre un protocolo se correlacionarían con la información sobre otro protocolo. Fue difícil capturar esto en la API.
La mayoría de las bases de datos tienen herramientas para importar y exportar archivos de texto delimitados de tablas. Pero a menudo solo quieres subconjuntos de ellos; haciendo uso de volcados de datos más complicado. En mi trabajo actual, necesitamos tomar algunos volcados de datos reales que son generados por los programas de Matlab y almacenados en la base de datos. Tenemos una herramienta que puede volcar un subconjunto de los datos de la base de datos y luego compararlo con la "verdad del terreno" para las pruebas. Parece que nuestras herramientas de extracción se modifican constantemente.
Prefiero crear los datos de prueba usando llamadas API.
Al comienzo de la prueba, crea una base de datos vacía (en memoria o la misma que se utiliza en producción), ejecuta el script de instalación para inicializarlo y luego crea los datos de prueba utilizados por la base de datos. La creación de los datos de prueba puede organizarse, por ejemplo, con el patrón Object Mother , de forma que los mismos datos puedan reutilizarse en muchas pruebas, posiblemente con pequeñas variaciones.
Desea tener la base de datos en un estado conocido antes de cada prueba, para tener pruebas reproducibles sin efectos secundarios. Por lo tanto, cuando finaliza una prueba, debe abandonar la base de datos de prueba o revertir la transacción, de modo que la siguiente prueba pueda volver a crear los datos de la prueba siempre de la misma manera, independientemente de si las pruebas anteriores pasaron o no.
La razón por la que evitaría importar volcados de bases de datos (o similares) es que acoplará los datos de prueba con el esquema de la base de datos. Cuando cambie el esquema de la base de datos, también deberá cambiar o volver a crear los datos de prueba, lo que puede requerir un trabajo manual.
Si los datos de la prueba están especificados en el código, tendrá a su disposición la potencia de las herramientas de refactorización de su IDE. Cuando realiza un cambio que afecta el esquema de la base de datos, probablemente también afecte las llamadas a la API, por lo que siempre deberá refactorizar el código utilizando la API. Con casi el mismo esfuerzo, también puede refactorizar la creación de los datos de prueba, especialmente si la refactorización puede ser automática (renombra, introduce parámetros, etc.). Pero si las pruebas se basan en un volcado de base de datos, necesitaría refactorizar manualmente el volcado de la base de datos además de refactorizar el código que usa la API.
Otra cosa relacionada con la prueba de integración de la base de datos es probar que la actualización desde un esquema de base de datos anterior funciona correctamente. Para eso es posible que desee leer el libro Refactoring Databases: Evolutionary Database Design o este artículo: http://martinfowler.com/articles/evodb.html
Utilicé DBUnit para tomar instantáneas de registros en una base de datos y almacenarlos en formato XML. Luego, mi unidad prueba (las llamamos pruebas de integración cuando necesitaban una base de datos), puede borrar y restaurar desde el archivo XML al comienzo de cada prueba.
No estoy seguro de si vale la pena el esfuerzo. Un problema son las dependencias de otras tablas. Dejamos solos las tablas de referencia estáticas y construimos algunas herramientas para detectar y extraer todas las tablas secundarias junto con los registros solicitados. Leí la recomendación de alguien para deshabilitar todas las claves externas en su base de datos de prueba de integración. Eso facilitaría la preparación de los datos, pero ya no verificará ningún problema de integridad referencial en sus pruebas.
Otro problema es el cambio de esquema de la base de datos. Escribimos algunas herramientas que agregarían valores predeterminados para las columnas que se agregaron desde que se tomaron las instantáneas.
Obviamente, estas pruebas fueron mucho más lentas que las pruebas de unidades puras.
Cuando intentas probar algún código heredado donde es muy difícil escribir pruebas unitarias para clases individuales, este enfoque puede valer la pena.
¿Por qué estos dos enfoques se definen como exclusivos?
No veo ningún argumento viable para no utilizar conjuntos de datos preexistentes, especialmente datos particulares que han causado problemas en el pasado.
No veo ningún argumento viable para no extender programáticamente esos datos con todas las condiciones posibles que pueda imaginar causando problemas e incluso un poco de datos aleatorios para las pruebas de integración .
En los enfoques ágiles modernos, las pruebas unitarias son donde realmente importa que se ejecuten las mismas pruebas cada vez. Esto se debe a que las pruebas unitarias están destinadas no a encontrar errores, sino a preservar la funcionalidad de la aplicación a medida que se desarrolla, lo que permite al desarrollador refactorizar según sea necesario.
Las pruebas de integración, por otro lado, están diseñadas para encontrar los errores que no esperabas. Correr con datos diferentes cada vez puede ser bueno, en mi opinión. Solo debe asegurarse de que su prueba preserve los datos fallidos si obtiene un error. Recuerde, en las pruebas de integración formal, la aplicación en sí se congelará, salvo las correcciones de errores, de modo que sus pruebas se puedan modificar para probar la cantidad máxima posible y los tipos de errores. En la integración, puede y debe arrojar el fregadero de la cocina en la aplicación.
Como otros han notado, por supuesto, todo esto naturalmente depende del tipo de aplicación que esté desarrollando y del tipo de organización en la que se encuentre, etc.