2-4 Réinitialisation de la BD de test
Lorsqu'on fait des tests automatisés, il est important que chaque test soit effectué dans un environnement contrôlé et un état connu. De plus, rien d'extérieur ne doit venir influencer l'état des tests.
Le meilleur état connu est un état vide! 😉 C'est pourquoi à chaque test, nous allons réinitialiser la base de données. Si un test requiert des données, ce dernier sera responsable d'en faire l'insertion. Ainsi, chaque test connait son environnement et l'état à partir duquel il est exécuté.
Nous pourrions donner un état de départ identique pour tous les tests, avec des données de test, mais à ce moment si un test ajoute des données pour supporter son besoin, il est possible que cela vienne interférer avec un autre test!
Bref, nous allons réinitialiser les données dans la base de données à chaque exécution de test. Pour cela, nous pourrions faire un MigrateDown()
et un MigrateUp()
, mais cela vient avec un coût puisque c'est une opération relativement longue. Au fond, nous ne voulons pas nécessairement complètement supprimer les tables et tout recommencer, on veut simplement vider le contenu.
La librairie Respawn
fait exactement cela! Elle effectue un truncate
de façon intelligente sur toutes les tables afin d'en vider le contenu.
Installer Respawn
- Dans le projet
Snowfall.Tests
->Manage NuGet Packages
- Faire une recherche pour
Respawn
- Ajouter
Respawn
au projet
Configurer Respawn via TestDatabaseFixture
Le concept de Fixture
est lié à toute classe ou entité qu'un test pourrait avoir besoin. Or, nos tests aurons besoin de réinitialiser la base de données de test via Respawn
. Nous allons donc créer une classe TestDatabaseFixture
.
-
Sous
Snowfall.Tests
, créez une classeTestDatabaseFixture
-
Faites ensuite hériter la classe de
IAsyncLifetime
pour qu'elle implémente les méthodesInitializeAsync()
(appelée avant chaque test) etDisposeAsync()
(appelée après chaque test)Snowfall.Tests/TestDatabaseFixture.cspublic class TestDatabaseFixture : IAsyncLifetime
{
public async Task InitializeAsync()
{
}
/// <summary>
/// Ferme la connexion SQL
/// </summary>
public async Task DisposeAsync()
{
}
} -
Lors de l'initialisation, nous devons charger la configuration de l'application dans
appsettings.Test.json
et initialiser Respawn.public class TestDatabaseFixture : IAsyncLifetime
{
private NpgsqlConnection? _connection;
private Respawner? _respawner;
private string? _connectionString;
public async Task InitializeAsync()
{
// Charger la configuration de l'application
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.Test.json")
.Build();
_connectionString = config.GetConnectionString("AppDatabaseConnection")!;
// Ouvrir une connection SQL pour Respawn
_connection = new NpgsqlConnection(_connectionString);
await _connection.OpenAsync();
// Créer l'instance de Respawn
_respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
{
DbAdapter = DbAdapter.Postgres,
TablesToIgnore = new Table[]
{
"VersionInfo",
"application_users",
"application_roles",
"application_roles_users"
}
});
}
/// <summary>
/// Ferme la connexion SQL
/// </summary>
public async Task DisposeAsync()
{
}
}À propos de TablesToIgnoreNous fournissons à Respawn une liste de table à ignorer pour réintialiser la base de données:
VersionInfo
: contient les migrations qui ont été exécutées. Nous ne voulons pas que les migrations soient réexécutée à chaque exécution d'un test, donc on garde cette table intacteapplication_users
,application_roles
etapplication_roles_users
: ce sera notre seule exception en lien avec l'état de la base de données qui doit être créée par chaque test. Comme les utilisateurs font partie de données assez fondamentales, nous laisserons ces données dans la BD.
-
Avant la réinitialisation, on veut s'assurer que les migrations aient bien été exécutées auparavant, au cas où on partirait d'une base de données complètement vide, sans aucune table.
using FluentMigrator.Runner;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Npgsql;
using Respawn;
using Respawn.Graph;
using Snowfall.Data.Configurations;
namespace Snowfall.Tests;
public class TestDatabaseFixture : IAsyncLifetime
{
private NpgsqlConnection? _connection;
private Respawner? _respawner;
private string? _connectionString;
public async Task InitializeAsync()
{
// Charger la configuration de l'application
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.Test.json")
.Build();
_connectionString = config.GetConnectionString("AppDatabaseConnection")!;
// Respawn doit avoir un schéma de base de données de prêt, on s'assure que la BD est migrée
EnsureDatabaseMigrated();
// Ouvrir une connection SQL pour Respawn
_connection = new NpgsqlConnection(_connectionString);
await _connection.OpenAsync();
// Créer l'instance de Respawn
_respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
{
DbAdapter = DbAdapter.Postgres,
TablesToIgnore = new Table[]
{
"VersionInfo",
"application_users",
"application_roles",
"application_roles_users"
}
});
}
/// <summary>
/// Exécute les migrations afin d'assurer un schéma de base de données
/// de départ complet
/// </summary>
private void EnsureDatabaseMigrated()
{
if (_connectionString is not null)
{
var services = new ServiceCollection()
.AddMigrations(_connectionString);
var sp = services.BuildServiceProvider();
sp.MigrateUp();
}
}
/// <summary>
/// Ferme la connexion SQL
/// </summary>
public async Task DisposeAsync()
{
}
} -
On peut ensuite implémenter la fonction
Dispose
pour qu'elle ferme essentiellement la connexion./// <summary>
/// Ferme la connexion SQL
/// </summary>
public async Task DisposeAsync()
{
if (_connection is not null)
{
await _connection.CloseAsync();
_connection.Dispose();
}
} -
Dernière étape, ajouter une fonction publique qu'il sera possible d'appeler à partir de nos tests pour réinitialiser la base de données.
/// <summary>
/// Réinitialise la base de données (données seulement).
/// </summary>
public async Task ResetDatabaseAsync()
{
if (_connection is not null && _respawner is not null)
{
await _respawner.ResetAsync(_connection);
}
}
Utiliser TestDatabaseFixture
dans les tests
-
Pour utiliser le fixture dans vos tests, vous pouvez faire hériter la classe de
IClassFixture<TestDatabaseFixture>
Snowfall.Tests/Web.Mvc/TestsIntegration/AccueilTests.cspublic class AccueilTests : IClassFixture<SnowfallMvcApplicationFactory>, IClassFixture<TestDatabaseFixture>
{
private readonly SnowfallMvcApplicationFactory _application;
private readonly TestDatabaseFixture _databaseFixture;
public AccueilTests(SnowfallMvcApplicationFactory application, TestDatabaseFixture databaseFixture)
{
_application = application;
_databaseFixture = databaseFixture;
} -
Ensuite, pour obtenir la méthode
InitializeAsync
qui nous permettra d'appelerResetDatabaseAsync
avant chaque test, on peut implémenterIAsyncLifetime
.public class AccueilTests : IClassFixture<SnowfallMvcApplicationFactory>, IClassFixture<TestDatabaseFixture>, IAsyncLifetime
{ -
Vous pouvez ensuite ajouter les deux fonctions requises par
IAsyncLifetime
public async Task InitializeAsync()
{
await _databaseFixture.ResetDatabaseAsync();
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}infoLors de l'initialisation, avant chaque test, la base de données est réinitialisée avec
ResetDatabaseAsync()
de la classeTestDatabaseFixture
.
Test
Vous pouvez maintenant essayer d'exécuter le test de base Obtenir_AccueilUtilisateurAnonyme_PageAfficheTexte
et le tout devrait être correctement exécuté.
De plus, si vous regardez dans votre base de données, vous remarquerez que vous aurez des données dans vos tables utilisateurs, mais toutes les autres tables devraient être vides!
Utiliser une collection xUnit
Pour que toutes les classes de test puissent utiliser la même connexion SQL et la même instance de Respawn, on peut créer une collection xUnit.
- Pour cela, dans votre fichier
TestDatabaseSixture
, ajoutez une classe qui hérite de votre fixture et qui déclare une collection.namespace Snowfall.Tests;
public class TestDatabaseFixture : IAsyncLifetime
{
private NpgsqlConnection? _connection;
private Respawner? _respawner;
private string? _connectionString;
public async Task InitializeAsync()
{
// Charger la configuration de l'application
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.Test.json")
.Build();
_connectionString = config.GetConnectionString("AppDatabaseConnection")!;
// Respawn doit avoir un schéma de base de données de prêt, on s'assure que la BD est migrée
EnsureDatabaseMigrated();
// Ouvrir une connection SQL pour Respawn
_connection = new NpgsqlConnection(_connectionString);
await _connection.OpenAsync();
// Créer l'instance de Respawn
_respawner = await Respawner.CreateAsync(_connection, new RespawnerOptions
{
DbAdapter = DbAdapter.Postgres,
TablesToIgnore = new Table[]
{
"VersionInfo",
"application_users",
"application_roles",
"application_roles_users"
}
});
}
/// <summary>
/// Exécute les migrations afin d'assurer un schéma de base de données
/// de départ complet
/// </summary>
private void EnsureDatabaseMigrated()
{
if (_connectionString is not null)
{
var services = new ServiceCollection()
.AddMigrations(_connectionString);
var sp = services.BuildServiceProvider();
sp.MigrateUp();
}
}
/// <summary>
/// Ferme la connexion SQL
/// </summary>
public async Task DisposeAsync()
{
if (_connection is not null)
{
await _connection.CloseAsync();
_connection.Dispose();
}
}
/// <summary>
/// Réinitialise la base de données (données seulement).
/// </summary>
public async Task ResetDatabaseAsync()
{
if (_connection is not null && _respawner is not null)
{
await _respawner.ResetAsync(_connection);
}
}
}
[CollectionDefinition("Test Database Collection")]
public class TestDatabaseCollection : ICollectionFixture<TestDatabaseFixture>
{
// Classe utilisée seulement pour la collection xUnit
} - Finalement, vous n'avez plus à faire hériter la classe de
TestDatabaseFixture
, vous pouvez ajouter un attribut avec le nom de la collection.Snowfall.Tests/Web.Mvc/TestsIntegration/AccueilTests.cs[Collection("Test Database Collection")]
public class AccueilTests : IClassFixture<SnowfallMvcApplicationFactory>, IAsyncLifetime
{