2-5 Premier test d'intégration (API)
Faisons un premier test d'intégration d'API, question d'avoir une base autant au niveau MVC que API pour la suite.
Nous allons répéter sensiblement le même processus, mais avec quelques différences.
Créer SnowfallApiApplicationFactory
Tout comme pour les tests du projet MVC
, il faudra une classe de type WebApplicationFactory
qui permettra de démarrer un serveur de test, mais pour l'API.
Pour ce faire, nous allons premièrement créer un dossier pour les tests liés au projet MVC.
- Sous
Snowfall.Tests
->Add
->Directory
- Nommer le dossier
Web.Api
Ensuite,
- Sous
Snowfall.Tests/Web.Api
->Add
->Class/Interface
- Nommer la classe
SnowfallApiApplicationFactory
namespace Snowfall.Tests.Web.Api;
public class SnowfallApiApplicationFactory
{
}
Faire hériter la classe SnowfallApiApplicationFactory
de WebApplicationFactory
Maintenant, on doit faire hériter la classe de WebApplicationFactory
afin qu'elle hérite des fonctionnalités requises permettant de créer un serveur de développement de test.
using Microsoft.AspNetCore.Mvc.Testing;
namespace Snowfall.Tests.Web.Api;
public class SnowfallApiApplicationFactory : WebApplicationFactory<>
{
}
On doit faire référence ici à Program.cs
du projet Api
. Tout comme pour MVC, le problème est que Program.cs
n'est pas un fichier/classe publique. Donc, on doit faire une manipulation pour rendre ce dernier public.
Modifier la portée de Program.cs
Pour modifier la portée de Program.cs
, on peut simplement ajouter ceci dans le bas:
//...
app.Run();
namespace Snowfall.Web.Api
{
public partial class Program { }
}
Faire référence à Program
dans SnowfallApiApplicationFactory
On peut maintenant faire référence à Program
(IMPORTANT: CELUI DE WEB.API) puisque ce dernier est public.
using Microsoft.AspNetCore.Mvc.Testing;
using Snowfall.Web.Api;
namespace Snowfall.Tests.TestsApi;
public class SnowfallApiApplication : WebApplicationFactory<Program>
{
}
Préciser l'environnement Test
Pour que l'application démarre avec la bonne configuration d'environnement, on peut le spécifier via la fonction CreateHost
:
namespace Snowfall.Tests.TestsApi;
public class SnowfallApiApplication : WebApplicationFactory<Program>
{
protected override IHost CreateHost(IHostBuilder builder)
{
builder.UseEnvironment("Test");
return base.CreateHost(builder);
}
}
Créer une première classe de test EvenementsTests
- Sous le dossier
Tests/Web.Api
, créer un dossierTestsIntegration
viaAdd
->Directory
- Sous le dossier
Tests/Web.Api
->Add
->Class/Interface
- Nommer la classe de tests
EvenementsTests
- Faire hériter la classe de test de
IClassFixture<SnowfallApiApplication>
,IAsyncLifetime
, en plus d'ajouter le nom de la collection xUnitTest Database Collection
afin de pouboir réinitialiser la base de données.Snowfall.Tests/Web.Api/TestsIntegration/EvenementsTests.cs[Collection("Test Database Collection")]
public class EvenementsTests : IClassFixture<SnowfallApiApplicationFactory>, IAsyncLifetime
{
public async Task InitializeAsync()
{
throw new NotImplementedException();
}
public async Task DisposeAsync()
{
throw new NotImplementedException();
}
} - Puis, on reçoit via le constructeur l'instance de l'application, ainsi que de
TestDatabaseFixture
Snowfall.Tests/TestsApi/TestsIntegration/EvenementsTests.cspublic class EvenementsTests : IClassFixture<SnowfallApiApplicationFactory>, IAsyncLifetime
{
private readonly SnowfallApiApplicationFactory _application;
private readonly TestDatabaseFixture _databaseFixture;
public EvenementsTests(SnowfallApiApplicationFactory application, TestDatabaseFixture databaseFixture)
{
_application = application;
_databaseFixture = databaseFixture;
} - Finalement, on réinitialise la base de données à chaque test
Snowfall.Tests/Web.Api/TestsIntegration/EvenementsTests.cs
public async Task InitializeAsync()
{
await _databaseFixture.ResetDatabaseAsync();
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}
Créer un premier test
Pour le premier test, on obtiendra la liste d'événements via l'API. Puisque nous n'avons rien configuré encore au niveau du support pour l'authentification, assurez-vous que l'action d'API correspondante n'est pas protégée (utiliser via AllowAnonymous
exemple).
// GET /api/evenements
[AllowAnonymous]
[HttpGet]
public async Task<IActionResult> Index()
Ensuite, créons le test pour récupérer la liste des événements en provenance de l'API.
[Fact]
public async Task Obtenir_ListeEvenements_RetourneListeSucces()
{
// Arrange
var client = _application.CreateClient();
string url = "api/evenements";
// Act
var evenements = await client.GetFromJsonAsync<List<EvenementDto>?>(url);
// Assert
Assert.NotNull(evenements);
Assert.NotEmpty(evenements);
}
Le principe est sensiblement le même que pour le test MVC
. On appelle cependant GetFromJsonAsync
pour recevoir le contenu en format JSON
, converti au type List<EvenementDto>
.
Ensuite, on valide que la liste n'est pas null, ni vide, et donc qu'elle contient des éléments.
Premier Test
Exécutez le test d'API tout juste créé et vous devriez obtenir l'erreur suivante ☹️
Xunit.Sdk.NotEmptyException
Assert.NotEmpty() Failure: Collection was empty
Le assert
vérifiant que la collection n'est pas vide NotEmpty
ne passe pas puisque la collection est vide! En effet, on réinitialise avant chaque test la base de données et elle est ainsi vide!
Il faudra faire en sorte que le test puisse insérer des événements dans la base de données avant le test.
Insertion des données pour le test
Le test est dépendant sur des événements, qui sont eux-mêmes dépendants sur des villes. Nous devrons donc:
- Insérer une ou plusieurs villes
- Insérer plusieurs événements qui seront associés aux villes créées.
Pour faire les insertions, nous avons déjà des repository qui sont responsables d'effectuer la majeure partie des opérations CRUD sur les modèles, nous pourrions donc réutiliser ces derniers. Nous devrons cependant les rendre disponibles au test via le système d'injection de dépendances.
Insertion d'une ville
-
Créer une fonction de repo
Create
pour créer une ville (nous ne l'avons pas!)Snowfall.Data/Repositories/VilleRepository.cspublic async Task<Ville> Create(Ville ville)
{
// À compléter!
}attentionLa fonction des à compléter!
-
Assurez-vous d'avoir l'interface associée
Snowfall.Data/Repositories/IVilleRepository.cspublic interface IVilleRepository
{
Task<List<Ville>> GetAll();
Task<Ville> Create(Ville ville);
} -
Dans la section
Arrange
du test, avant de créer le client et tout le reste, récupérez le repo via l'injection de dépendances.Snowfall.Tests/Web.Api/TestsIntegration/EvenementsTests.cspublic async Task Obtenir_ListeEvenements_RetourneListeSucces()
{
// Arrange
var scope = _application.Services.CreateScope();
var villeRepository = scope.ServiceProvider.GetRequiredService<IVilleRepository>();
//... -
Créez une ville à insérer
public async Task Obtenir_ListeEvenements_RetourneListeSucces()
{
// Arrange
var scope = _application.Services.CreateScope();
var villeRepository = scope.ServiceProvider.GetRequiredService<IVilleRepository>();
var ville = new Ville()
{
Nom = "Paris",
PaysIso = "fr"
}; -
Procédez à l'insertion
Snowfall.Tests/Web.Api/TestsIntegration/EvenementsTests.cspublic async Task Obtenir_ListeEvenements_RetourneListeSucces()
{
// Arrange
var scope = _application.Services.CreateScope();
var villeRepository = scope.ServiceProvider.GetRequiredService<IVilleRepository>();
var ville = new Ville()
{
Nom = "Paris",
PaysIso = "fr"
};
ville = await villeRepository.Create(ville);
Insertion d'une liste d'événements
De la même façon que précédemment, nous pouvons maintenant procéder à l'insertion d'une liste d'événements et les associer à la ville nouvellement créée.
-
Récupérer le repo d'événements et créer une liste d'événements à insérer.
public async Task Obtenir_ListeEvenements_RetourneListeSucces()
{
// Arrange
var scope = _application.Services.CreateScope();
var villeRepository = scope.ServiceProvider.GetRequiredService<IVilleRepository>();
var ville = new Ville()
{
Nom = "Paris",
PaysIso = "fr"
};
ville = await villeRepository.Create(ville);
var evenementRepository = scope.ServiceProvider.GetRequiredService<IEvenementRepository>();
List<Evenement> evenementsList = new()
{
new Evenement()
{
Nom = "Evenement de test",
Description = "Description de l'événement",
Capacite = 10,
Prix = 100,
Date = DateTime.Now,
ImagePath = "image.jpg",
VilleId = ville.Id,
},
new Evenement()
{
Nom = "Autre evenement de test",
Description = "Description de l'événement",
Capacite = 100,
Prix = 200,
Date = DateTime.Now,
ImagePath = "image.jpg",
VilleId = ville.Id,
}
};
//...infoRemarquez que les événements sont liés à la ville nouvellement créée!
-
Procédez à l'insertion pour chaque événement de la liste
//...
var evenementRepository = scope.ServiceProvider.GetRequiredService<IEvenementRepository>();
List<Evenement> evenementsList = new()
{
new Evenement()
{
Nom = "Evenement de test",
Description = "Description de l'événement",
Capacite = 10,
Prix = 100,
Date = DateTime.Now,
ImagePath = "image.jpg",
VilleId = ville.Id,
},
new Evenement()
{
Nom = "Autre evenement de test",
Description = "Description de l'événement",
Capacite = 100,
Prix = 200,
Date = DateTime.Now,
ImagePath = "image.jpg",
VilleId = ville.Id,
}
};
foreach (var evenement in evenementsList)
{
await evenementRepository.Create(evenement);
}
//...
Test
Exécutez le test et ce dernier devrait maintenant fonctionner puisqu'une liste d'événements est présente en BD! 🎉
Nettoyage
À partir d'ici, il existe plusieurs approches pour nettoyer le test. Les éléments que nous sommes susceptibles de répéter sont les suivants:
- La création de scope et récupération des repositories
- Le client HTTP
- Les fonctions
Dispose
et d'initialisation pour réinitialiser la BD - Optionnellement, les insertions en BD.
Nous allons créer une classe de base sur laquelle toutes les classes de test d'intégration pourront s'appuyer.
- Sous le dossier
Snowfall.Tests/Web.Api/TestsIntegration
, créez une nouvelle classe appeléeWebApiIntegrationTestBase
. - Déplacez les éléments en lien avec l'injection de dépendance, la BD et le client HTTP dans cette classe de base.
Snowfall.Tests/Web.Api/TestsIntegration/WebApiIntegrationTestBase.cs
using Microsoft.Extensions.DependencyInjection;
using Snowfall.Data.Repositories;
namespace Snowfall.Tests.Web.Api.TestsIntegration;
/// <summary>
/// Classe de base pour les tests d'intégration d'API.
/// Gère l'injection de dépendance, la réinitialisation de la BD et le client HTTP.
/// </summary>
[Collection("Test Database Collection")]
public abstract class WebApiIntegrationTestBase :
IClassFixture<SnowfallApiApplicationFactory>,
IAsyncLifetime,
IDisposable
{
protected readonly SnowfallApiApplicationFactory Application;
protected readonly TestDatabaseFixture Database;
protected HttpClient Client { get; private set; } = null!;
protected readonly IVilleRepository VilleRepository;
protected readonly IEvenementRepository EvenementRepository;
private readonly IServiceScope _scope;
protected WebApiIntegrationTestBase(
SnowfallApiApplicationFactory application,
TestDatabaseFixture database)
{
Application = application;
Database = database;
_scope = Application.Services.CreateScope();
VilleRepository = _scope.ServiceProvider.GetRequiredService<IVilleRepository>();
EvenementRepository = _scope.ServiceProvider.GetRequiredService<IEvenementRepository>();
}
/// <summary>
/// Avant chaque test, crée un nouveau client et réinitialise la BD.
/// </summary>
public async Task InitializeAsync()
{
await Database.ResetDatabaseAsync();
Client = Application.CreateClient();
}
public Task DisposeAsync() => Task.CompletedTask;
public void Dispose() => _scope.Dispose();
} - Faites en sorte que la classe
EvenementsTests
hérite de la classe de baseSnowfall.Tests/Web.Api/TestsIntegration/EvenementsTests.cspublic class EvenementsTests : WebApiIntegrationTestBase
- Retirez l'attribut en lien avec la collection puisque ce dernier est dans la classe de base
[Collection("Test Database Collection")]
public class EvenementsTests : WebApiIntegrationTestBase - Retirez les attributs en lien avec l'application factory et
TestDatabaseFixture
public class EvenementsTests : WebApiIntegrationTestBase
{
private readonly SnowfallApiApplicationFactory _application;
private readonly TestDatabaseFixture _databaseFixture; - Modifiez le constructeur pour qu'il appelle simplement le constructeur de la classe de base.
public class EvenementsTests : WebApiIntegrationTestBase
{
public EvenementsTests(
SnowfallApiApplicationFactory application,
TestDatabaseFixture database) : base(application, database)
{ } - Retirez les fonctions
InitializeAsync
etDisposeAsync
- Finalement, faites en sorte que la fonction de test utilise
Client
,VilleRepository
etEvenementRepository
de la classe de base, en plus de supprimer les appels descope
pour l'injection de dépendance.public class EvenementsTests : WebApiIntegrationTestBase
{
public EvenementsTests(
SnowfallApiApplicationFactory application,
TestDatabaseFixture database) : base(application, database)
{ }
[Fact]
public async Task Obtenir_ListeEvenements_RetourneListeSucces()
{
// Arrange
var ville = new Ville()
{
Nom = "Paris",
PaysIso = "fr"
};
ville = await VilleRepository.Create(ville);
List<Evenement> evenementsList = new()
{
new Evenement()
{
Nom = "Evenement de test",
Description = "Description de l'événement",
Capacite = 10,
Prix = 100,
Date = DateTime.Now,
ImagePath = "image.jpg",
VilleId = ville.Id,
},
new Evenement()
{
Nom = "Autre evenement de test",
Description = "Description de l'événement",
Capacite = 100,
Prix = 200,
Date = DateTime.Now,
ImagePath = "image.jpg",
VilleId = ville.Id,
}
};
foreach (var evenement in evenementsList)
{
await EvenementRepository.Create(evenement);
}
string url = "api/evenements";
// Act
var evenements = await Client.GetFromJsonAsync<List<EvenementDto>?>(url);
// Assert
Assert.NotNull(evenements);
Assert.NotEmpty(evenements);
}
}
Vous pourriez vouloir mettre dans des fonctions les blocs de seed de données pour les sortir de la fonction de test.
Je vous laisse le soin d'implémenter cette portion si vous la trouvez pertinente pour vos tests.