Compartir a través de


Elección de una estrategia de pruebas

Como se describe en la Información general, debe decidir si las pruebas implicarán al sistema de base de datos de producción (igual que la aplicación) o si las pruebas se ejecutarán con un doble de prueba, que reemplazará al sistema de base de datos de producción.

Las pruebas en un recurso externo real, en lugar de reemplazarlo por un doble de prueba, pueden implicar las siguientes dificultades:

  1. En muchos casos, simplemente no es posible ni práctico realizar pruebas en el recurso externo real. Por ejemplo, puede que la aplicación interactúe con algún servicio que no se pueda probar fácilmente (debido a la limitación de velocidad o a la falta de un entorno de prueba).
  2. Incluso cuando es posible implicar al recurso externo real, es algo que puede ser muy lento: ejecutar una gran cantidad de pruebas en un servicio en la nube puede hacer que las pruebas tarden demasiado tiempo. Las pruebas deben formar parte del flujo de trabajo diario del desarrollador, por lo que es importante que las pruebas se ejecuten rápidamente.
  3. La ejecución de pruebas en un recurso externo puede implicar problemas de aislamiento, por los que las pruebas interfieren entre sí. Por ejemplo, varias pruebas que se ejecutan en paralelo en una base de datos pueden modificar los datos y provocar un error entre sí de varias maneras. Usar un doble de prueba evita esto, ya que cada prueba se ejecuta en su propio recurso en memoria y, por tanto, está aislado naturalmente de otras pruebas.

Sin embargo, las pruebas que se superan con un doble de prueba no garantizan que el programa funcione al ejecutarse en el recurso externo real. Por ejemplo, puede que un doble de prueba de base de datos haga comparaciones de cadenas que distinguen mayúsculas de minúsculas, mientras que el sistema de base de datos de producción hace comparaciones que no distinguen mayúsculas de minúsculas. Estos problemas solo se descubren cuando las pruebas se ejecutan en la base de datos de producción real, lo que hace que estas pruebas sean una parte importante de cualquier estrategia de pruebas.

Las pruebas en la base de datos pueden ser más fáciles de lo que parece

Debido a las dificultades descritas anteriormente que supone realizar pruebas en una base de datos real, a los desarrolladores se les insta con frecuencia a usar los dobles de prueba primero y a tener un conjunto de pruebas sólido que se pueda ejecutar con frecuencia en sus máquinas; en cambio, se supone que las pruebas que implican la base de datos se ejecutan con mucha menos frecuencia y, en muchos casos, también proporcionan una cobertura mucho menor. Se recomienda plantearse realizar estas últimas pruebas y se sugiere que, en realidad, las bases de datos pueden verse mucho menos afectadas por los problemas anteriores de lo que se tiende a pensar:

  1. En la actualidad, la mayoría de las bases de datos se pueden instalar fácilmente en la máquina del desarrollador. Las tecnologías basadas en contenedores, como Docker, pueden facilitar esta tarea, y las tecnologías como las áreas de trabajo de Github y el contenedor de desarrollo configuran todo el entorno de desarrollo (incluida la base de datos). Al usar SQL Server, también es posible realizar pruebas con LocalDB en Windows o configurar fácilmente una imagen de Docker en Linux.
  2. Las pruebas en una base de datos local (con un conjunto de datos de prueba razonable) suelen ser extremadamente rápidas: la comunicación es completamente local y los datos de prueba se suelen almacenar en búfer en la memoria en la base de datos. EF Core contiene más de 30 000 pruebas solo en SQL Server; estas se completan de forma fiable en unos minutos, se ejecutan en CI en cada confirmación y son ejecutadas localmente por los desarrolladores con mucha frecuencia. Algunos desarrolladores recurren a una base de datos en memoria (una base de datos "falsa") creyendo que esto es necesario para la velocidad, pero, en realidad, casi nunca es el caso.
  3. El aislamiento es realmente un obstáculo al ejecutar pruebas en una base de datos real, ya que las pruebas pueden modificar datos e interferir entre sí. Sin embargo, hay varias técnicas para proporcionar aislamiento en los escenarios de pruebas de base de datos; nos centramos dichas técnicas en Pruebas en el sistema de base de datos de producción.

Con lo anterior, no se pretende menospreciar a los dobles de prueba ni desaconsejar su uso. Por ejemplo, los dobles de prueba son necesarios para algunos escenarios que no se pueden probar de otro modo, como simular errores de base de datos. Sin embargo, según nuestra experiencia, los usuarios suelen evitar realizar pruebas en su base de datos por los motivos anteriores, ya que creen que es algo lento, difícil o poco fiable, cuando no es necesariamente el caso. Realizar pruebas en el sistema de base de datos de producción pretende abordar esto, ya que proporciona instrucciones y ejemplos para escribir pruebas rápidas y aisladas en la base de datos.

Diferentes tipos de dobles de prueba

Doble de prueba es un término amplio que abarca enfoques muy diferentes. En esta sección se tratan algunas técnicas comunes que implican dobles de prueba para probar aplicaciones de EF Core:

  1. Usar SQLite (modo en memoria) como una base de datos falsa que reemplaza el sistema de base de datos de producción.
  2. Usar el proveedor en memoria de EF Core como una base de datos falsa que reemplaza el sistema de base de datos de producción.
  3. Simular o procesar con código auxiliar DbContext y DbSet.
  4. Introducir una capa de repositorio entre EF Core y el código de la aplicación, y simular dicha capa o usar código auxiliar en la misma.

A continuación, exploraremos lo que significa cada método y lo compararemos con los demás. Se recomienda leer los diferentes métodos para entender cada uno por completo. Si ha decidido escribir pruebas que no implican el sistema de base de datos de producción, la capa de repositorio es el único enfoque que permite simular la capa de datos o usar código auxiliar en la misma de manera completa y fiable. Sin embargo, dicho enfoque tiene un costo significativo en términos de implementación y mantenimiento.

SQLite como una base de datos falsa

Un posible enfoque de prueba consiste en intercambiar la base de datos de producción (por ejemplo, SQL Server) con SQLite, usándola de forma efectiva como una prueba "falsa". Además de facilitar la configuración, SQLite tiene una característica de base de datos en memoria que es especialmente útil para las pruebas: cada prueba está aislada naturalmente en su propia base de datos en memoria y no es necesario administrar archivos reales.

Sin embargo, antes de hacerlo, es importante comprender que, en EF Core, los distintos proveedores de bases de datos se comportan de forma diferente: EF Core no intenta abstraer todos los aspectos del sistema de base de datos subyacente. Fundamentalmente, esto significa que las pruebas en SQLite no garantizan los mismos resultados que en SQL Server ni en ninguna otra base de datos. Estos son algunos ejemplos de posibles diferencias de comportamiento:

  • La misma consulta LINQ puede devolver resultados diferentes en distintos proveedores. Por ejemplo, SQL Server realiza una comparación de cadenas que no distingue mayúsculas de minúsculas de forma predeterminada, mientras que SQLite distingue mayúsculas de minúsculas. Esto puede hacer que las pruebas se superen en SQLite y que no se superen en SQL Server (o viceversa).
  • Algunas consultas que funcionan en SQL Server simplemente no se admiten en SQLite, ya que la compatibilidad exacta con SQL difiere en estas dos bases de datos.
  • Si la consulta usa un método específico del proveedor, como EF.Functions.DateDiffDay de SQL Server, dicha consulta producirá un error en SQLite y no se podrá probar.
  • En función de lo que se haga exactamente, puede que SQL sin formato funcione, produzca un error o devuelva resultados diferentes. Los dialectos de SQL son diferentes de muchas maneras en las bases de datos.

En comparación con la ejecución de pruebas en el sistema de base de datos de producción, es relativamente fácil empezar a trabajar con SQLite, y muchos usuarios lo hacen. Desafortunadamente, las limitaciones anteriores suelen acabar siendo problemáticas al probar aplicaciones de EF Core, incluso si no parecen dar problemas al principio. Por tanto, se recomienda escribir las pruebas en la base de datos real o, si usar un doble de prueba es absolutamente necesario, asumir el costo de un patrón de repositorio como se describe a continuación.

Para obtener información sobre cómo usar SQLite para pruebas, consulte esta sección.

El proveedor en memoria como una base de datos falsa

Como alternativa a SQLite, EF Core también incluye un proveedor en memoria. Aunque este proveedor se diseñó originalmente para admitir pruebas internas de EF Core, algunos desarrolladores lo usan como una base de datos falsa al probar aplicaciones de EF Core. Hacer esto es muy desaconsejable: como base de datos falsa, el proveedor en memoria tiene los mismos problemas que SQLite (véase anteriormente) y, además, tiene las siguientes limitaciones adicionales:

  • Por lo general, el proveedor en memoria admite menos tipos de consulta que el proveedor de SQLite, ya que no es una base de datos relacional. En comparación con la base de datos de producción, se producirá un error en más consultas o estas se comportarán de forma diferente.
  • No se admiten las transacciones.
  • No se admite por completo SQL sin formato. Compare esto con SQLite, donde es posible usar SQL sin formato, siempre que SQL funcione de la misma manera en SQLite y en la base de datos de producción.
  • El proveedor en memoria no se ha optimizado para el rendimiento y, por lo general, funcionará más lentamente que SQLite en modo en memoria (o incluso el sistema de base de datos de producción).

En resumen, el proveedor en memoria tiene todas las desventajas de SQLite, y algunas adicionales, y no ofrece ninguna ventaja a cambio. Si busca una base de datos simple y en memoria falsa, use SQLite en lugar del proveedor en memoria; pero considere la posibilidad de usar el patrón de repositorio como se describe a continuación.

Para obtener información sobre cómo usar el proveedor en memoria para realizar pruebas, consulte esta sección.

Simulación o código auxiliar de DbContext and DbSet

Este enfoque suele usar un marco ficticio para crear un doble de prueba de DbContext y DbSet y realiza pruebas en dichos dobles. La simulación de DbContext puede ser un buen enfoque para probar varias funciones de que no son de consulta, como llamadas a Add o SaveChanges(), lo que le permite comprobar que el código los llamó en escenarios de escritura.

Sin embargo, no es posible simular correctamente la funcionalidad de consulta de DbSet, ya que las consultas se expresan a través de operadores LINQ, que son llamadas de método de extensión estáticas que se realizan mediante IQueryable. Como resultado, cuando algunas personas hablan de "simular DbSet", se refieren a que crean un DbSet respaldado por una colección en memoria y, a continuación, evalúan los operadores de consulta en dicha colección en memoria, al igual que un simple IEnumerable. Más que una simulación, esto es realmente un tipo de emulación en la que la colección en memoria reemplaza a la base de datos real.

Dado que solo se falsifica DbSet y la consulta se evalúa en memoria, este enfoque termina siendo muy similar al uso del proveedor en memoria de EF Core: ambas técnicas ejecutan operadores de consulta en .NET a través de una colección en memoria. Como resultado, esta técnica tiene las mismas desventajas: las consultas se comportarán de forma diferente (por ejemplo, con respecto a la distinción entre mayúsculas y minúsculas) o simplemente producirán errores (por ejemplo, debido a métodos específicos del proveedor), SQL sin procesar no funcionará y, en el mejor de los casos, las transacciones se omitirán. Como resultado, en general esta técnica debe evitarse al probar cualquier código de consulta.

Modelo de repositorio

Los enfoques anteriores intentan cambiar el proveedor de bases de datos de producción de EF Core por un proveedor de pruebas falso o crear un DbSet respaldado por una colección en memoria. Estas técnicas se parecen en que todavía evalúan las consultas LINQ del programa (ya sea en SQLite o en memoria) y, en última instancia, esto da lugar a las dificultades descritas anteriormente: una consulta diseñada para ejecutarse en una base de datos de producción específica no se puede ejecutar de forma fiable en otro lugar sin causar problemas.

Para usar un doble de prueba adecuado y fiable, considere la posibilidad de introducir una capa de repositorio, que media entre el código de la aplicación y EF Core. La implementación de producción del repositorio contiene las consultas LINQ reales y las ejecuta a través de EF Core. En las pruebas, la abstracción del repositorio se procesa directamente con código auxiliar o se simula sin necesidad de ninguna consulta LINQ real, lo que elimina EF Core efectivamente de la pila de pruebas y permite que las pruebas se centren solo en el código de la aplicación.

En el diagrama siguiente se compara el enfoque de base de datos falsa (SQLite o en memoria) con el patrón de repositorio:

Comparison of fake provider with repository pattern

Dado que las consultas LINQ ya no forman parte de las pruebas, puede proporcionar directamente los resultados de la consulta a la aplicación. Dicho de otra manera, los enfoques anteriores permiten aproximadamente procesar entradas de consulta con código auxiliar (por ejemplo, reemplazar tablas de SQL Server por tablas en memoria), pero después siguen ejecutando los operadores de consulta reales en memoria. En cambio, el patrón de repositorio le permite procesar salidas de consulta con código auxiliar directamente, lo que permite realizar pruebas unitarias mucho más eficaces y enfocadas. Tenga en cuenta que, para que esto funcione, el repositorio no debe exponer ningún método que devuelva IQueryable, ya que estos, de nuevo, no se pueden procesar con código auxiliar; en su lugar, se debe devolver IEnumerable.

Sin embargo, dado que el patrón de repositorio requiere encapsular cada consulta LINQ que se puede probar en un método que devuelva IEnumerable, impone una capa de arquitectura adicional en la aplicación y puede suponer un costo significativo para implementarlo y mantenerlo. Se debe tener en cuenta este costo al decidir cómo probar una aplicación, especialmente porque es probable que las pruebas en la base de datos real sigan siendo necesarias para las consultas expuestas por el repositorio.

Cabe destacar que los repositorios tienen ventajas aparte de la realización de pruebas. Garantizan que todo el código de acceso a datos esté concentrado en un solo lugar, en vez de distribuirse por la aplicación; además, si la aplicación necesita admitir más de una base de datos, la abstracción del repositorio puede ser muy útil para ajustar las consultas entre proveedores.

Para ver un ejemplo que muestra la realización de pruebas con un repositorio, consulte esta sección.

Comparación general

En la tabla siguiente se proporciona una vista comparativa rápida de las distintas técnicas de prueba y se muestra qué funcionalidad se puede probar con cada enfoque:

Característica En memoria SQLite en memoria Simulación de DbContext Modelo de repositorio Pruebas en la base de datos
Tipo de doble de prueba Falso Falso Falso Simulación o código auxiliar Real, no doble
¿SQL sin formato? No Depende No
¿Transacciones? No (se omite)
¿Traducciones específicas del proveedor? No Ninguno No
¿Comportamiento exacto de las consultas? Depende Depende Depende
¿Puede usar LINQ en cualquier parte de la aplicación? No*

* Todas las consultas LINQ de base de datos que se pueden probar deben encapsularse en métodos de repositorio que devuelvan IEnumerable, con el fin de simularlas o procesarlas con código auxiliar.

Resumen

  • Se recomienda que los desarrolladores ejecuten una buena cobertura de pruebas de su aplicación en su sistema de base de datos de producción real. Esto permite confiar en que la aplicación funciona realmente en producción y en que, con el diseño adecuado, las pruebas se pueden ejecutar de manera fiable y rápida. Dado que en cualquier caso es preciso realizar estas pruebas, es una buena idea empezar desde ahí y, si hace falta, se pueden agregar pruebas con dobles de prueba más adelante, según sea necesario.
  • Si ha decidido usar un doble de prueba, se recomienda implementar el patrón de repositorio, que le permite procesar la capa de acceso a datos con código auxiliar o simularla por encima de EF Core, en lugar de usar un proveedor falso de EF Core (Sqlite o en memoria) o simular DbSet.
  • Si el patrón de repositorio no es una opción viable por algún motivo, considere la posibilidad de usar bases de datos en memoria de SQLite.
  • Evite el proveedor en memoria con fines de prueba: es algo que se desaconseja y solo se admite con aplicaciones heredadas.
  • Evite simular DbSet con fines de consulta.