En este post vamos a ver qué son y para qué sirven los test de integración dentro de nuestro código.
Este no es el único post que tenemos en la web sobre los test dentro de C#, anteriormente hemos visto qué son los test unitarios y qué es mock dentro de los tests si no tienes claro cualquiera de estos conceptos, recomiendo encarnecidamente que les heches un vistazo, ya que todo va a ser crucial a la hora de entender los test de integración.
Además, en este post veremos el ejemplo con Docker Con lo que si no tienes claro qué es docker, o como funciona también recomiendo echarle un vistazo, aunque no es crucial, ya que en el escenario lo utilizaremos únicamente para contener nuestra base de datos.
Finalmente utilizaremos el código utilizado durante el curso web c#.
Índice
1 - Qué son los test de integración?
Como vimos en el post de los test unitarios disponemos de una pirámide de test, la cual contiene en la base los propios test unitarios y como segunda capa los test de integración. En esta segunda capa es donde nos vamos a centrar durante este post.
Los test de integración son aquellos que nos permiten testear las integraciones de nuestro proyecto.
Esto quiere decir que no vamos a testear que un algoritmo funciona correctamente, sino que comprobaremos que el código funciona de principio a fin.
Por lo tanto el punto principal de los test de integración es verificar que nuestro sistema (o una parte del mismo) funciona correctamente cuando estos son utilizados conjuntamente.
2 - Test de integración y test unitarios
Los test de integración y los test unitarios se utilizan en conjunto, no son exclusivos unos de otros, de hecho, deberíamos tener ambos tipos de test en todos los proyectos que los requieran.
La forma más sencilla de ver esto es con un ejemplo:
El punto principal de un test unitario es verificar que una pequeña parte de nuestro proyecto, como puede ser una función para calcular la mayoría de edad funciona correctamente.
bool EsMayorDeEdad(int edad) => edad >= 18;
Por otra parte, cuando ejecutamos un test de integración queremos saber que ningún usuario menor de edad puede acceder a la web. Por lo que estamos testeando que el código ejecuta la decisión correcta basándose en el resultado del método EsMayorDeEdad
pero sin importarnos como ese dato es calculado.
2.1 - Por qué necesitamos test de integración y test unitarios?
El motivo principal por el que necesitamos ambos tipos de test es básicamente porque la combinación de escenarios que el código puede tomar es casi infinita, imaginémonos que el punto de entrada son 10 argumentos pasados por parámetro, escribir test con todas las combinaciones posibles nos llevaría cientos o incluso miles de tests.
Pero si simplemente verificamos en scopes muy pequeños y luego todos en conjunto, junto con los más relevantes o los más probables junto con los resultados de los test unitarios podemos obtener una valoración positiva de que nuestro sistema está funcionando correctamente.
3 - Características de los test de integración
Comúnmente cuando creamos test de integración vamos a querer reproducir escenarios reales, lo que nos da una vista más cercana a producción. Además, dependiendo de la dificultad de los test manuales podemos enfocar nuestro desarrollo al Test driven design.
Muchas empresas, tienen el code coverage, o cuánta parte del código tenemos cubierta como un estigma. Esto está bien y mal, porque si solo tienes un caso, puedes cubrir todo o la mayoría pero no “excepciones”. Aún así, los test de integración nos permiten cubrir la mayoría del código.
Desafortunadamente los test de integración pueden ser MUY difíciles de desarrollar, por ejemplo en nuestro caso en concreto de este post, vamos a necesitar una aplicación adicional en nuestro proyecto para restaurar la base de datos.
Debemos tener en cuenta que los test se corren en paralelo, por lo que comprobar IDs hardcoded no es una opción.
4 - Ejemplo Test de integración con C# y Docker
El motivo por el que vamos a implementar los test de integración con docker es porqué para su ejecución necesitamos una base de datos completamente limpia, para comprobar que nuestros casos y nuestro escenario funciona correctamente.
Este método nos permite además en caso de tener CI/CD repetiremos el proceso en uno de los pasos, asegurándonos que el código en su conjunto también funciona en el servidor o en la nube.
4.1 - Implementar la base de datos en Docker
Primero de todo decir que NO recomiendo tener una base de datos en Docker para producción, pero obviamente si para los test.
La idea principal es replicar nuestro servidor de bases de datos en una máquina pero gracias a Docker NO necesitamos una máquina como tal, sino que simplemente hacemos la imagen y la corremos. En ella indicamos la base de datos y la contraseña.
FROM mysql:5.6
ENV MYSQL_DATABASE webpersonal
ENV MYSQL_ROOT_PASSWORD=test
## todos los scripts en docker-entrypoint-initdb.d/ se ejecutan automaticamente
COPY ./Database/ ./docker-entrypoint-initdb.d/
Sobre esta imagen de docker con MySQL
ya corriendo, debemos ejecutar los ficheros sql que componen la base de datos.
Podemos hacer este paso tanto el la imagen como tal, creando una aplicación de consola en .Net, o en un pequeño script de powershell.
En mi caso personal, lo tengo en un pequeño script de powershell el cual deberá ser ejecutado antes de empezar a trabajar con los test de integración. Ya que no solo ejecuta los ficheros sql, sino que inicializa la imagen de docker.
##Copiar ficheros de la base de datos
$source = "src/Database"
$destino = "Tools/ServerMysql"
Copy-Item -Path $source -Filter "*.sql" -Recurse -Destination $destino -Container -force
##Borrar la imagen vieja
docker rm $(docker stop $(docker ps -a -q --filter ancestor='server-mysql' --format="{{.ID}}"))
##construir la imagen
docker build -t server-mysql Tools\ServerMysql\.
##iniciar el contenedor
docker run -d -p 4306:3306 server-mysql
observamos que estamos indicando el puerto que va a estar escuchando, en mysql el normal es 3306
pero es probable que tengamos un mysql corriendo en nuestro ordenador, con lo que en ese caso podemos utilizar uno random cualquiera, en mi caso he puesto 4306
.
Como vimos en el post de la estructura de una aplicación debemos ubcar este fichero en el sitio correcto, en nuestro caso, sera en la raiz del proyecto y ahí una carpeta llamada Tools donde incluiremos todo aquello que utilzamos para hacernos la vida mas sencilla, pero no es directamente parte del cóigo como tal.
Por cierto, se recomienda correr los scripts directamente desde la raíz del proyecto.
Por lo tanto ejecutaremos el siguiente comando para construir nuestro servidor mysql con la información necesaria en un contenedor de docker:
PS C:\repos\WebPersonal> .\Tools\ServerMySql\BuildServerAndRun.ps1
4.2 - Creación de un test de integración con Visual studio
Para crear un test de integración en visual studio debemos crear un proyecto de test y a este llamarlo TestDeIntegracion, exactamente igual que hicimos con los test unitarios.
Por supuesto debemos crearlo en su carpeta correspondiente.
La idea de los test de integración es cubrir todo el sistema, tanto front como back end, en nuestro caso por ahora no tenemos front end, lo que no quiere decir que nos libremos de hacer los test de integración, sino que cuando el front esté listo, habrá que hacer una refactorización.
Primero de todo debemos tener en cuenta que, si nuestro proyecto utiliza inyeccion de dependencias deberemos inyectar estas dependencias en nuestro test. Haré un vídeo sobre “inyección de dependencias” avanzado donde refactorizar.
Para el ejemplo están puestas “a pelo”
private IServiceCollection BuildDependencies()
{
IServiceCollection services = new ServiceCollection();
services.AddScoped<DbConnection>(x
=> new MySqlConnection("Server=127.0.0.1;Port=4306;Database=webpersonal;Uid=root;password=test;Allow User Variables=True"))
.AddScoped<TransactionalWrapper>()
.AddScoped<PersonalProfile>()
.AddScoped<PutPersonalProfile>()
.AddScoped<IGetPersonalProfileDependencies, GetPersonalProfileDependencies>()
.AddScoped<IPutPersonalProfileDependencies, PutPersonalProfileDependencies>()
.AddScoped<PersonalProfileRepository>()
.AddScoped<SkillRepository>()
.AddScoped<InterestsRepository>()
.AddScoped<UserIdRepository>();
return services;
}
Podemos observar además que la conexión mysql está apuntando a nuestro contenedor docker. Este paso también va a necesitar de refactorización cuando veamos una implementación de los diferentes entornos.
Por lo tanto creamos el test que comprobara que el flow o el proceso funciona:
[Fact]
public async Task TestInsertPerfilPersonal_Then_ModifyIt()
{
IServiceCollection services = BuildDependencies();
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
string username = Guid.NewGuid().ToString();
PersonalProfileDto defaultPRofile = BuildPersonalProfile(username);
var departmentAppService = serviceProvider.GetRequiredService<PerfilPersonalController>();
await departmentAppService.Post(defaultPRofile);
PersonalProfileDto userStep1 = await departmentAppService.Get(username).ThrowAsync();
Assert.Empty(userStep1.Skills);
Assert.Equal(defaultPRofile.FirstName, userStep1.FirstName);
Assert.Equal(defaultPRofile.Website, userStep1.Website);
Assert.Equal(defaultPRofile.LastName, userStep1.LastName);
SkillDto skill = new SkillDto()
{
Id = null,
Name = "nombre1",
Punctuation = 10m
};
userStep1.Skills.Add(skill);
InterestDto interest = new InterestDto()
{
Id = null,
Interest = "interes pero debe contener 15 caracteres"
};
userStep1.Interests.Add(interest);
var _ =await departmentAppService.Put(userStep1);
PersonalProfileDto userStep2 = await departmentAppService.Get(username).ThrowAsync();
Assert.Single(userStep2.Skills);
Assert.Equal(skill.Name, userStep2.Skills.First().Name);
Assert.Single(userStep2.Interests);
Assert.Equal(interest.Interest, userStep2.Interests.First().Interest);
Assert.Equal(defaultPRofile.FirstName, userStep2.FirstName);
Assert.Equal(defaultPRofile.Website, userStep2.Website);
Assert.Equal(defaultPRofile.LastName, userStep2.LastName);
}
}
private PersonalProfileDto BuildPersonalProfile(string uniqueUsername)
{
return new PersonalProfileDto()
{
Description = "Description",
Email = "email",
FirstName = "firstName",
GitHub = "github",
Id = null,
Interests = new List<InterestDto>(),
LastName = "last name",
Phone = "telefono",
Skills = new List<SkillDto>(),
UserId = null,
UserName = uniqueUsername,
Website = "web"
};
}
Es muy común ejecutar los resultados contra la base de datos directamente, en nuestro caso particular “get” únicamente lee de la base de datos, por lo que el efecto y resultado son el mismo.
Conclusión
Los test de integración son un proceso y un paso que debemos implementar en todos nuestros desarrollos para asegurarnos de que nuestro código cuando lo ponemos todo junto funciona correctamente.
Como hemos visto son complicados de ejecutar, ya que hay que invertir cantidad de tiempo en implementar todos los servicios asociados (siempre que estén en el proyecto), como puede ser la base de datos en Docker para este escenario. Por ello debemos combinar test de integración junto con test unitarios para un completo testeo de la aplicación.