Este es el tercer artículo de la serie Domion - Un sistema para desarrollar aplicaciones en .NET Core. En el artículo anterior desarrollamos los componentes iniciales de la aplicación modelo, haciendo énfasis en el patrón de repositorio con Entity Framework Core (EF Core), que implementamos y denominamos, de forma general, como EntityManagers.
En esta ocasión nos vamos en enfocar en desarrollar unas pruebas básicas de integración para los EntityManagers, no sólo con el objetivo de verificar su funcionamiento, sino también para que sirvan como los bloques básicos para construir las pruebas de aceptación con SpecFlow, siguiendo el enfoque BDD, que veremos más adelante en la serie.
También aprovecharemos luego este aprendizaje para desarrollar nuevas plantillas para generar este tipo de componentes y seguir ampliando el alcance de Domion.
Puntos Importantes
Pruebas de integración con xUnit y FluentAssertions.
Mover o renombrar proyectos de una solución en VS 2017.
Cambiar el TargetFramework en proyectos .NET Core.
Entender la forma correcta de hacer pruebas de integración sobre un DbContext.
Aplicar el patrón de pruebas Arrange / Act / Assert.
Refactorización usando delegados.
Estandarizar pruebas típicas como paso previo a generarlas usando MDA.
Al terminar el artículo tendremos una buena estructura para organizar las pruebas de integración y podremos apreciar las ventajas de trabajar con el enfoque “Code First” en Entity Framework Core.
Para el artículo siguiente tenemos planeado trabajar con:
- Inyección de dependencias usando AutoFac.
Programas fuente
Artículo: Domion.Net-3.0.zip (release 3.0 del repositorio)
Repositorio: https://github.com/mvelosop/Domion.Net
ImportanteSi quiere realizar el tutorial paso a paso, le recomiendo que comience con los fuentes del release 2.0 (.zip)
El tiempo estimado para realizar este tutorial es de aproximadamente una hora.
Contexto
Después de varios años desarrollando productos (que requieren mantenimiento) y haber pagado el precio de haber sido un poco laxo con el tema de las pruebas, he llegado a la conclusión de que las pruebas automatizadas son una de las mejores inversiones que podemos realizar para mejorar la productividad de la empresa, cuando hay que dar mantenimiento a los programas y no se factura por hora.
Esto puede parecer contradictorio, ya que las pruebas requieren tiempo adicional para desarrollarlas y luego para mantenerlas, pero, en mi humilde opinión, sólo hace falta tener un enfoque pragmático en las pruebas.
En mi segunda iteración profesional en desarrollo de software, usando .NET, lo que me ha dado mejor resultado valor/costo, ha sido trabajar casi todas las historias de usuario partiendo desde las pruebas de aceptación usando BDD con SpecFlow, pero hacerlo desde la capa de negocio, incluyendo la base de datos, sin pasar por la interfaz de usuario.
Además, en cuanto a las pruebas unitarias, hacerlas sólo en los casos complejos, como algunas máquinas de estado, por ejemplo.
Y la verdad es que, aunque me había funcionado bien, me daba cierta vergüenza decirlo, hasta que un día escuché a Scott Allen en .Net Rocks y luego encontré esta respuesta en Stack Overflow del mismísimo Kent Beck, considerado el padre de TDD.
Todavía no vamos a hablar sobre BDD, sino que vamos a comenzar con las pruebas de integración, como ya lo hemos mencionado.
Sin embargo, estamos apuntando a facilitar el desarrollo de las pruebas o, mejor dicho, las especificaciones con SpecFlow.
Herramientas y plataforma
Visual Studio 2017 Community Edition
(ver la página de descargas de Visual Studio para otras versiones)..NET Core SDK 1.0.4 - x64 Installer
(ver la página de descargas de .NET Core para otras versiones)
Paquetes NuGet utilizados
- FluentAssertions - 4.19.2
- Microsoft.EntityFrameworkCore - 1.1.2
- Microsoft.EntityFrameworkCore.Design - 1.1.2
- Microsoft.EntityFrameworkCore.SqlServer - 1.1.2
- Microsoft.NET.Test.Sdk - 15.0.0
- NLog - 5.0.0-beta07
- System.ComponentModel.Annotations - 4.3.0
- xunit - 2.2.0
- xunit.runner.visualstudio - 2.2.0
A - Mover proyecto de pruebas en la solución
Preparando el artículo me di cuenta que las pruebas que vamos a hacer corresponden al proyecto de prueba y no directamente a las librerías, aunque, obviamente al probar la aplicación también estamos probándolas, pero me parece más adecuado renombrar la carpeta tests a samples.tests, para reflejar más claramente lo que son.
ImportanteEstrictamente hablando, probablemente sería más fácil eliminar y crear los proyectos de nuevo, porque están vacíos, pero es útil saber cómo hacer esta operación, para cuando sea realmente necesario.
A-1 - Renombrar carpetas en Visual Studio
Desde el explorador de la solución:
- Cambiar nombre de la carpeta tests por samples.test
- Cerar la solución, salvando el archivo .sln cuando Visual Studio pregunte si quiere salvar los cambios
A-2 - Renombrar carpetas del sistema de archivos
Desde el explorador de archivos:
- Cambiar nombre de la carpeta tests por samples.test
- Editar el archivo Domion.Net.sln y cambiar las línea donde aparecen las rutas originales de los proyectos de pruebas por las rutas nuevas:
Rutas originales ("tests\...
)
Project("{FAE ... FBC}") = "DFlow.Budget.Lib.Tests", "tests\DFlow.Budget.Lib.Tests\DFlow.Budget.Lib.Tests.csproj", ...
Project("{9A1 ... 556}") = "DFlow.Transactions.Lib.Tests", "tests\DFlow.Transactions.Lib.Tests\DFlow.Transactions.Lib.Tests.csproj", ...
Rutas nuevas ("samples.tests\...
)
Project("{FAE ... FBC}") = "DFlow.Budget.Lib.Tests", "sample.tests\DFlow.Budget.Lib.Tests\DFlow.Budget.Lib.Tests.csproj", ...
Project("{9A1 ... 556}") = "DFlow.Transactions.Lib.Tests", "samples.tests\DFlow.Transactions.Lib.Tests\DFlow.Transactions.Lib.Tests.csproj", ...
A-3 - Abrir la solución y recompilar
Al abrir la solución se deberían ver los proyectos en el explorador de la solución y recompilar sin errores.
A-4 - Cambiar las referencias de los proyectos
En nuestro caso, todavía no hay ninguna referencia hacia los proyectos de prueba, pero si las hubiese, sería necesario cambiarlas en los archivos .csproj de los proyectos correspondientes.
A-5 - En caso de emergencia
En caso de que no logre realizar este proceso con éxito, probablemente lo mejor es borrar el proyecto por completo y crearlo de nuevo.
ImportanteRecuerde que al crear el proyecto sobre una carpeta de solución en Visual Studio, debe seleccionar a mano la carpeta en el sistema de archivos.
B - Preparación del ambiente
ImportanteAl escribir el artículo (15/06/2017), por alguna razón que no logré identificar y corregir, falló el explorador de pruebas de Visual Studio 2017 y, al no encontrar las pruebas, no podía ejecutarlas.
Sin embargo pude encontrar una vuelta, cambiando el TargetFramework de los proyectos a .NET Framework 4.6.2.
En esta sección entonces vamos a cambiar el TargetFramework de los proyectos, además de instalar los paquetes necesarios, para no tener que enfrentarnos con el problema mencionado.
B-1 - Cambiar el TargetFramework (plataforma)
¿Qué significa cambiar el target framework?
Significa que después de hacerlo, aunque vamos a seguir desarrollando en .NET Core, sólo vamos a poder correr los programas en Windows.
Si el target framework fuera .NET Core, podríamos correrlo en cualquier plataforma soportada, por ejemplo, Linux.
De todas formas, eventualmente resolverán este problema y podremos volver a .NET Core.
ImportanteCuando trabajamos con .NET Core podemos incluso utilizar varios TargetFrameworks, por ejemplo .NET Core y .NET Framework 4.6.2, y así generar los ejecutables para ambas plataformas.
Lamentablemente el explorador de pruebas tampoco funcionaba al trabajar con las dos plataformas simultáneamente.
B-1.1 - Descargar todos los proyectos de la solución
En teoría se debería poder hacer sin necesidad de descargar los proyectos, pero me ha resultado más rápido hacerlo así.
- Seleccionar todos los proyectos en el explorador de la solución
- [Botón derecho > Unload Project]
B-1.2 - Modificar DFlow.Budget.Core.csproj
El archivo .csproj se modifica con [Botón derecho > Edit {nombre del proyecto}.csproj] sobre el proyecto en el explorador de la solución.
Para cambiar la plataforma hay que cambiar el tag del TargetFramework de .NET Core 1.1 (netcoreapp1.1
)
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>
a .NET Framework 4.6.2 (net462
)
<PropertyGroup>
<TargetFramework>net462</TargetFramework>
</PropertyGroup>
Además de cambiar la plataforma, en el proyecto DFlow.Budget.Core, es necesario incluir un referencia a externa a System.ComponentModel.Annotations cuando el target framework sea “net462”, así que el archivo DFlow.Budget.Core.csproj debe quedar así:
|
|
Note que la inclusión de la referencia está condicionada al TargetFramework net462
No es necesario hacer esto para netcoreapp1.1, porque el paquete System.ComponentModel.Annotations ya está incluído en .NET Core.
B-1.3 - Editar archivos .csproj
Para el resto de los proyectos sólo es necesario hacer el cambio de la plataforma, editando el archivo .csproj para cambiar netcoreapp1.1 por net462.
B-1.4 - Recargar todos los proyectos de la solución
- Seleccionar todos los proyectos en el explorador de la solución
- [Botón derecho > Reload Project]
En este momento debería poder compilar la solución sin errores.
B-2 - Crear proyecto src\Domion.FluentAssertions
Como estamos preparando el ambiente, vamos a crear de una vez el proyecto indicado, porque lo vamos a necesitar en un momento.
El proyecto se debe crear como Class Library (.NET Core)
ImportanteRecuerde que debe seleccionar manualmente la carpeta “src” al crear el proyecto.
Para este proyecto también tenemos que hacer el cambio de plataforma e incluir el paquete System.ComponentModel.Annotations, así que vamos a modificar el archivo Domion.FluentAssertions.csproj a esto:
|
|
Con esto también instalaremos de una vez el paquete FluentAssertions que vamos a necesitar.
B-3 - Configurar proyecto de pruebas
B-3.1 - Crear archivo de configuración de xUnit
Crear el archivo xunit.runner.json en la raíz del proyecto de pruebas:
|
|
Este archivo hace que el explorador de pruebas muestre el sólo nombre de los métodos de prueba (en vez de mostrar también el nombre completo de la clase):
Ajustar las propiedades del archivo ([Alt]+[Enter] o [Botón derecho > Properties] sobre el explorador de la solución) para que siempre se copie a la carpeta de salida (ejecutables).
C - Pruebas de integración básicas
En esta sección del artículo vamos desarrollar una versión inicial básica de las pruebas de integración.
Incluso, vamos a comenzar con una versión que ni siquiera ha pasado por una refactorización, para explicar el proceso de llegar a una estructura mucho más cómoda de usar.
C-1 - Trabajar con DbContext
ImportanteEs importante tener en cuenta la forma adecuada de trabajar con un DbContext, ya que no seguir las recomendaciones nos puede traer problemas difíciles de diagnosticar y resolver.
Un DbContext es, entre otras cosas, un cache, con sus ventajas e inconvenientes, así que es necesario estar consciente de eso.
Entonces, como para efectos de esta serie estamos enfocados en el desarrollo de aplicaciones web, donde la vida de cada DbContext está limitada a un request, tenemos que simular ese ciclo de vida en las pruebas, para que éstas se parezcan más a las condiciones reales de producción.
Esto quiere decir que un DbContext (que implementa IDisposable) se debe utilizar dentro de una estructura “using” (using (var dbContext = new DbContext()) { }
) para “delimitar” los pasos que normalmente se realizarían durante un request y estar seguros que el DbContext se descarta al terminar el using.
C-1.1 - BudgetClassData - Datos de prueba
Para facilitar el manejo de los datos de prueba, vamos a usar una clase que representa los datos ingresados por el usuario.
Además, como veremos en un artículo posterior, esto es especialmente útil cuando tenemos que hacer referencia a otros objetos
|
|
C-1.2 - Estructura de las pruebas con un DbContext
ImportantePara efectos de las pruebas de todo tipo, tanto unitarias como de integración y aceptación o comportamiento (BDD), se usa el patrón Arrange / Act / Assert, en el cuál:
- Arrange: Crea el contexto para realizar la prueba.
- Act: Ejecuta lo que se quiere probar.
- Assert: Verifica que se hayan obtenido los resultados esperados.
Al combinar esto con lo indicado en el punto anterior, resulta que una prueba típica tiene la siguiente estructura, donde lo que más destaca es un using () { }
para cada fase (Arrange, Act, Assert) como se muestra a continuación:
|
|
Según lo que hemos comentado, debería ser bastante clara la necesidad del using () {}
en la fase Act, pero ¿Por qué en el Arrange y el Assert?
Porque de esa forma, al usar instancias diferentes del DbContext, nos aseguramos de evitar resultados incorrectos (tanto falsos positivos como negativos) por objetos que pueden quedar en el ChangeTracker del DbContext, como resultado de las operaciones anteriores.
C-2 - Refactorización
Con una inspección rápida del código anterior es evidente que hay varias oportunidades de refactorización.
Lo primero que vamos a trabajar son las fases de Arrange y Assert, en éstas encontramos cuatro casos fundamentales.
Para que las pruebas sean repetibles es necesario:
- Asegurar que algunas entidades existan en la base de datos y/o
- Asegurar que algunas entidades no existan en la base de datos
Y para terminar, después de ejecutar que función que se está probando, en la mayoría de los casos, es necesario:
- Verificar si existen algunas entidades en la base de datos y/o
- Verificar si no existen algunas entidades en la base de datos
No vamos a ver ahora los detalles de implementación de esos métodos, porque son bastante obvios, pero lo importante es que vamos a usar una clase “Helper” que resuelva esos detalles y ésta va a necesitar una instancia del EntityManager para hacerlo.
C-2.1 - Manager Helper
El punto importante es que, después de implementar lo que sería el BudgetClassManagerHelper se reducen significativamente las secciones:
Arrange
// Ensure entitiy does not exist
using (var dbContext = dbSetupHelper.GetDbContext())
{
var manager = new BudgetClassManager(dbContext);
var helper = new BudgetClassManagerHelper(manager);
helper.EnsureEntitiesDoNotExist(data);
}
Assert
// Verify entity exists
using (var dbContext = dbSetupHelper.GetDbContext())
{
var manager = new BudgetClassManager(dbContext);
var helper = new BudgetClassManagerHelper(manager);
helper.AssertEntitiesExist(data);
}
Sin embargo, todavía hay algo por mejorar ahí, aunque a lo mejor no es tan evidente cómo hacerlo.
La solución se basa en el uso de delegados, específicamente un Action, porque en ambos casos el contexto del using es el mismo, sólo cambia lo que se ejecuta dentro.
C-2.2 - Refactorizando el contexto y la verificación
ImportanteAquí vamos a ver una estrategia interesante de refactorización usando delegados.
Entonces sólo tenemos que implementar un método que resuelva el contexto y reciba como parámetro lo que se va a ejecutar.
Además, como sabemos que se va a trabajar con el helper, se lo podemos pasar como parámetro al Action para que sea todavía más fácil usarlo:
private void UsingManagerHelper(Action<BudgetClassManagerHelper> action)
{
using (var dbContext = dbSetupHelper.GetDbContext())
{
var manager = new BudgetClassManager(dbContext);
var helper = new BudgetClassManagerHelper(manager);
action.Invoke(helper);
}
}
Después, con este nuevo método, ahora sí se reduce significativamente el código y el nombre de los métodos es completamente explicativo:
Arrange
// Ensure entitiy does not exist
UsingManagerHelper(helper =>
{
helper.EnsureEntitiesDoNotExist(data);
});
Assert
// Verify entity exists
UsingManagerHelper(helper =>
{
helper.AssertEntitiesExist(data);
});
C-2.3 - Refactorizando la sección de prueba (Act)
De forma similar, también podemos refactorizar la sección de prueba para obtener esto:
private void UsingManager(Action<BudgetClassManager> action)
{
using (BudgetDbContext dbContext = DbSetupHelper.GetDbContext())
{
var manager = new BudgetClassManager(dbContext);
action.Invoke(manager);
}
}
Ahora, aplicando todas las refactorizaciones, la prueba queda bastante simplificada respecto a la versión inicial:
|
|
C-3 - Refactorización y versión final
Para no extender demasiado el artículo, a continuación presentamos las versiones finales de las clases resultantes.
Las clases que se muestran a continuación se pueden copiar e incluir directamente en el proyecto.
C-3.1 - BudgetClassDataMapper - Convertidor entre datos y entidades
Entre esta clase y BudgetClassData, se implementa el patrón Mapper para establecer la comunicación entre las pruebas y las librerías (.Core y .Lib) del módulo, haciendo la conversión BudgetClass <–> BudgetClassData.
Este patrón, va a resultar especialmente útil en los escenarios más complejos que veremos más adelante en la serie.
|
|
C-3.2 - BudgetClassManagerHelper - Asistente del EntityManager
En esta clase se implementan los cuatro puntos mencionados en C-2 - Refactorización.
Adicionalmente a lo que se indicó en ese punto, se puede ver que en los métodos Assert… se están creando nuevos DbContext y EntityManager.
Esto se hace para poder llamarlos desde los Ensure… y estar seguros que no estamos trabajando con el mismo DbContext donde se realizó la operación.
|
|
C-3.3 - FluentAssertionsExtensions - Extensión para facilitar la verificación de errores
En esta clase se implementa una extensión sobre la librería FluentAssertions, para facilitar la verificación de los mensajes de error en las validaciones.
Aquí se compara si en la colección de mensajes recibidos, hay alguno que comience con la parte constante del mensaje esperado.
La parte constante del mensaje va desde el comienzo del string hasta la posición del primer parámetro de sustitución (el primer “{”).
Esta clase la vamos a incluir en el proyecto src\Domion.FluentAssertions que creamos anteriormente.
|
|
C-3.4 - BudgetClassManager_IntegrationTests - Pruebas de integración
Finalmente llegamos a las pruebas de integración, donde implementamos las siguientes pruebas básicas:
- Se pueden insertar entidades.
- Se pueden modificar entidades.
- Se pueden eliminar entidades.
- No se puede insertar una entidad con nombre duplicado.
- No se puede modificar el nombre de una entidad si ya existe otra con el nuevo nombre, porque se duplicaría.
|
|
ImportanteUno de los aspectos más interesantes de estas pruebas, es que logramos un nivel de estandarización, con el que podemos generar con MDA los bloques básicos de pruebas, que luego nos ayuden a desarrollar más rápidamente especificaciones ejecutables con SpecFlow para usar el enfoque BDD como pruebas de aceptación.
C-4 - Ejecución de las pruebas
Después de compilar la solución, se debe ver todas las pruebas en el explorador de pruebas ([Menú Principal > TEST > Windows > Test Explorer]):
Y se deben ejecutar todas (Run All) correctamente:
Y si consultamos la base de datos que se creó usando el string de conexión de la clase de pruebas BudgetClassManager_IntegrationTests, deberíamos ver esto:
ImportanteUno de los aspectos más interesantes de esta forma de trabajo es que no nos hemos tenido que ocupar de la base de datos, aparte del string de conexión a utilizar.
Al correr las pruebas se va a crear automáticamente la base de datos y siempre va a tener los mismos datos y al final de las pruebas siempre va a quedar en el mismo estado (si todo va bien), así que cuando tengamos algún problema será mucho más fácil hacer el diagnóstico.
C-5 - Repetición de las pruebas
Las pruebas se pueden ejecutar tantas veces como se quiera y siempre darán los mismos resultados, con la única diferencia de los Id de algunos registros que cambiarán con cada ejecución, por la eliminación de algunos registros en las secciones de Arrange de las pruebas.
En caso necesario, se puede eliminar por completo la base de datos como se indica a continuación, porque se creará de nuevo automáticamente al ejecutar las pruebas:
Resumen
En este artículo trabajamos le desarrollo de pruebas básicas de integración trabajando con xUnit y Entity Framework Core y construimos las bases para otros escenarios más complejos e interesantes que exploraremos más adelante.
También aprendimos algunos detalles importantes sobre el trabajo y las pruebas con los DbContext.
Espero que este artículo le haya resultado útil y le invito a darme su opinión en la sección de comentarios.
Gracias,
Miguel.Enlaces relacionados
Action
https://msdn.microsoft.com/en-us/library/018hxwa8(v=vs.110).aspx
AutoFac
https://autofac.org/
BDD
https://en.wikipedia.org/wiki/Behavior-driven_development
DbContext Life cycle
https://msdn.microsoft.com/en-us/library/jj729737(v=vs.113).aspx#Anchor_1
DbContext
https://docs.microsoft.com/en-us/ef/core/api/microsoft.entityframeworkcore.dbcontext
Entity Framework Core
https://docs.microsoft.com/en-us/ef/core/index
FluentAssertions
http://fluentassertions.com/
Kent Beck
https://en.wikipedia.org/wiki/Kent_Beck
Kent Beck, sobre TDD en Stack Overflow
https://stackoverflow.com/questions/153234/how-deep-are-your-unit-tests#answer-153565
MDA
https://en.wikipedia.org/wiki/Model-driven_architecture
Patrón de repositorio
https://martinfowler.com/eaaCatalog/repository.html
Patrón Mapper
https://martinfowler.com/eaaCatalog/mapper.html
Scott Allen en .Net Rocks
http://www.dotnetrocks.com/?show=1405
Scott Allen
http://odetocode.com/about/scott-allen
SpecFlow
http://specflow.org/
TDD
https://en.wikipedia.org/wiki/Test-driven_development
xUnit
https://xunit.github.io/