Test sans votre système de base de données de production
Dans cette page, nous abordons les techniques d’écriture de tests automatisés n’impliquant pas le système de base de données sur lequel l’application s’exécute en production. Pour ce faire, votre base de données est remplacée par un double de test. Il existe différents types d’approches et de doubles de tests, il est donc recommandé de lire attentivement la documentation Choix d’une stratégie de test pour comprendre pleinement les différentes options. Enfin, il est également possible de tester sur votre système de base de données de production ; ceci est abordé dans Test sur votre système de base de données de production.
Conseil
Cette page présente des techniques xUnit, mais des concepts similaires existent dans d’autres infrastructures de test, notamment NUnit.
Modèle de référentiel
Si vous avez décidé d’écrire des tests sans impliquer votre système de base de données de production, la technique recommandée pour ce faire est le modèle de référentiel ; pour plus d’informations sur ce sujet, consultez cette section. La première étape de l’implémentation du modèle de référentiel consiste à extraire vos requêtes EF Core LINQ dans un calque distinct, que nous allons ensuite utiliser avec un stub ou en simulation. Voici un exemple d’interface de référentiel pour notre système de blogs :
public interface IBloggingRepository
{
Task<Blog> GetBlogByNameAsync(string name);
IAsyncEnumerable<Blog> GetAllBlogsAsync();
void AddBlog(Blog blog);
Task SaveChangesAsync();
}
... et voici un exemple partiel d’implémentation pour une utilisation en production :
public class BloggingRepository : IBloggingRepository
{
private readonly BloggingContext _context;
public BloggingRepository(BloggingContext context)
=> _context = context;
public async Task<Blog> GetBlogByNameAsync(string name)
=> await _context.Blogs.FirstOrDefaultAsync(b => b.Name == name);
// Other code...
}
Il n’y a pas grand-chose à lui : le référentiel encapsule simplement un contexte EF Core et expose des méthodes qui exécutent les requêtes de base de données et les mises à jour dessus. Un point clé à noter est que notre méthode GetAllBlogs
retourne IAsyncEnumerable<Blog>
(ou IEnumerable<Blog>
), et non IQueryable<Blog>
. Le renvoi de ce dernier signifierait que les opérateurs de requête pourraient toujours être composés sur le résultat, exigeant qu’EF Core soit toujours impliqué dans la traduction de la requête ; ce qui mettrait en échec l’objectif d’avoir un référentiel en premier lieu. IAsyncEnumerable<Blog>
nous permet facilement d’utiliser un stub ou de simuler ce que le référentiel renvoie.
Pour une application ASP.NET Core, nous devons inscrire le référentiel en tant que service dans l’injection de dépendances en ajoutant ce qui suit au ConfigureServices
de l’application :
services.AddScoped<IBloggingRepository, BloggingRepository>();
Enfin, nos contrôleurs sont injectés avec le service de référentiel au lieu du contexte EF Core et exécutent des méthodes sur celle-ci :
private readonly IBloggingRepository _repository;
public BloggingControllerWithRepository(IBloggingRepository repository)
=> _repository = repository;
[HttpGet]
public async Task<Blog> GetBlog(string name)
=> await _repository.GetBlogByNameAsync(name);
À ce stade, votre application est conçue en fonction du modèle de référentiel : le seul point de contact avec la couche d’accès aux données - EF Core - est maintenant via la couche référentiel, qui agit comme médiateur entre le code de l’application et les requêtes de base de données réelles. Les tests peuvent désormais être écrits simplement en utilisant le stub dans le référentiel, ou en mode fictif avec votre bibliothèque de simulation préférée. Voici un exemple de test basé sur des simulacres à l’aide de la bibliothèque populaire Moq :
[Fact]
public async Task GetBlog()
{
// Arrange
var repositoryMock = new Mock<IBloggingRepository>();
repositoryMock
.Setup(r => r.GetBlogByNameAsync("Blog2"))
.Returns(Task.FromResult(new Blog { Name = "Blog2", Url = "http://blog2.com" }));
var controller = new BloggingControllerWithRepository(repositoryMock.Object);
// Act
var blog = await controller.GetBlog("Blog2");
// Assert
repositoryMock.Verify(r => r.GetBlogByNameAsync("Blog2"));
Assert.Equal("http://blog2.com", blog.Url);
}
L’exemple de code complet peut être consulté ici.
SQLite en mémoire
SQLite peut facilement être configuré en tant que fournisseur EF Core pour votre suite de tests au lieu de votre système de base de données de production (e.g. SQL Server) ; consultez la documentation fournisseur SQLite pour plus d’informations. Toutefois, il est généralement judicieux d’utiliser la fonctionnalité de base de données en mémoire SQLite lors du test, car elle offre une isolation facile entre les tests et ne nécessite pas de traiter les fichiers SQLite réels.
Pour utiliser SQLite en mémoire, il est important de comprendre qu’une nouvelle base de données est créée chaque fois qu’une connexion de bas niveau est ouverte et qu’elle est supprimée lorsque cette connexion est fermée. Dans une utilisation normale, l'DbContext
d’EF Core ouvre et ferme les connexions de base de données si nécessaire , chaque fois qu’une requête est exécutée, afin d’éviter de conserver la connexion inutilement longtemps. Toutefois, avec SQLite en mémoire, cela entraînerait une réinitialisation de la base de données à chaque fois ; ainsi, pour contourner ce problème, nous allons ouvrir la connexion avant de la transmettre à EF Core et organiser sa fermeture uniquement lorsque le test se termine :
public SqliteInMemoryBloggingControllerTest()
{
// Create and open a connection. This creates the SQLite in-memory database, which will persist until the connection is closed
// at the end of the test (see Dispose below).
_connection = new SqliteConnection("Filename=:memory:");
_connection.Open();
// These options will be used by the context instances in this test suite, including the connection opened above.
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlite(_connection)
.Options;
// Create the schema and seed some data
using var context = new BloggingContext(_contextOptions);
if (context.Database.EnsureCreated())
{
using var viewCommand = context.Database.GetDbConnection().CreateCommand();
viewCommand.CommandText = @"
CREATE VIEW AllResources AS
SELECT Url
FROM Blogs;";
viewCommand.ExecuteNonQuery();
}
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
BloggingContext CreateContext() => new BloggingContext(_contextOptions);
public void Dispose() => _connection.Dispose();
Les tests peuvent à présent appeler CreateContext
, qui renvoie un contexte à l’aide de la connexion que nous avons configurée dans le constructeur, assurant ainsi la mise à disposition d’une base de données propre avec les données allouées.
L’exemple de code complet pour les tests en mémoire SQLite peut être consulté ici.
Fournisseur en mémoire
Comme indiqué dans la page vue d’ensemble des tests , l’utilisation du fournisseur en mémoire pour les tests est fortement déconseillée ; envisagez d’utiliser SQLite à la place, ou implémenter le modèle de référentiel. Si vous avez décidé d’utiliser en mémoire, voici un constructeur de classe de test classique qui configure et crée une nouvelle base de données en mémoire avant chaque test :
public InMemoryBloggingControllerTest()
{
_contextOptions = new DbContextOptionsBuilder<BloggingContext>()
.UseInMemoryDatabase("BloggingControllerTest")
.ConfigureWarnings(b => b.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
using var context = new BloggingContext(_contextOptions);
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.AddRange(
new Blog { Name = "Blog1", Url = "http://blog1.com" },
new Blog { Name = "Blog2", Url = "http://blog2.com" });
context.SaveChanges();
}
L’exemple de code complet pour les tests en mémoire peut être consulté ici.
Nommage de base de données en mémoire
Les bases de données en mémoire sont identifiées par un nom de chaîne simple, et il est possible de se connecter à la même base de données plusieurs fois en fournissant le même nom (c’est pourquoi l’exemple ci-dessus doit appeler EnsureDeleted
avant chaque test). Toutefois, notez que les bases de données en mémoire sont enracinées dans le fournisseur de services interne du contexte ; même si, dans la plupart des cas, les contextes partagent le même fournisseur de services, la configuration de contextes avec différentes options peut déclencher l’utilisation d’un nouveau fournisseur de services interne. Lorsque c’est le cas, transmettez explicitement la même instance de InMemoryDatabaseRoot à UseInMemoryDatabase
pour tous les contextes qui doivent partager des bases de données en mémoire (cela est généralement effectué en ayant un champ de InMemoryDatabaseRoot
statique).
Transactions
Notez que, par défaut, si une transaction est démarrée, le fournisseur en mémoire lève une exception, car les transactions ne sont pas prises en charge. Vous souhaiterez peut-être ignorer les transactions silencieusement en configurant EF Core pour ignorer InMemoryEventId.TransactionIgnoredWarning
comme dans l’exemple ci-dessus. Toutefois, si votre code s’appuie réellement sur la sémantique transactionnelle ; par exemple, s’il dépend de la restauration réelle des modifications, votre test ne fonctionnera pas.
Affichages
Le fournisseur de données en mémoire permet de définir des vues à l'aide de requêtes LINQ avec ToInMemoryQuery:
modelBuilder.Entity<UrlResource>()
.ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));