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 Choisir une stratégie de test pour comprendre pleinement les différentes options. Enfin, il est également possible de réaliser des tests sur votre système de base de données de production ; cela est expliqué 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 dans ce cas 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
{
    Blog GetBlogByName(string name);

    IEnumerable<Blog> GetAllBlogs();

    void AddBlog(Blog blog);

    void SaveChanges();
}

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 Blog GetBlogByName(string name)
        => _context.Blogs.FirstOrDefault(b => b.Name == name);

    // Other code...
}

Il n’y a pas de grande différence : le référentiel encapsule simplement un contexte EF Core et expose des méthodes qui exécutent les requêtes de la base de données et les mises à jour. Un point clé à retenir est que notre méthode GetAllBlogs renvoie 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 que 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. IEnumerable<Blog> nous permet facilement d’utiliser un stub ou de simuler ce que le référentiel retourne.

Pour une application ASP.NET Core, nous devons enregistrer le référentiel en tant que service dans l’injection de dépendances en ajoutant les éléments suivants à l’application ConfigureServices :

services.AddScoped<IBloggingRepository, BloggingRepository>();

Ainsi, 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 celui-ci :

private readonly IBloggingRepository _repository;

public BloggingControllerWithRepository(IBloggingRepository repository)
    => _repository = repository;

[HttpGet]
public Blog GetBlog(string name)
    => _repository.GetBlogByName(name);

À ce stade, votre application est conçue suivant le modèle de référentiel : le seul point de contact avec la couche d’accès aux données EF Core se fait désormais via le calque du référentiel, qui agit comme un 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 fictif à l’aide de la bibliothèque populaire Moq :

[Fact]
public void GetBlog()
{
    // Arrange
    var repositoryMock = new Mock<IBloggingRepository>();
    repositoryMock
        .Setup(r => r.GetBlogByName("Blog2"))
        .Returns(new Blog { Name = "Blog2", Url = "http://blog2.com" });

    var controller = new BloggingControllerWithRepository(repositoryMock.Object);

    // Act
    var blog = controller.GetBlog("Blog2");

    // Assert
    repositoryMock.Verify(r => r.GetBlogByName("Blog2"));
    Assert.Equal("http://blog2.com", blog.Url);
}

L’exemple de code complet est disponible ici.

SQLite In-Memory

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 (par exemple, SQL Server). Consultez la documentation du 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 de SQLite lors du test, car elle permet une isolation facile entre les tests et ne nécessite pas la gestion de fichiers SQLite réels.

Pour utiliser SQLite in-memory, 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, la commande DbContextde EF Core ouvre et ferme les connexions de base de données si nécessaire, et ce, chaque fois qu’une requête est exécutée. Cela évite de maintenir la connexion inutilement pendant de longues périodes. Toutefois, avec SQLite in-memory, cela entraînerait une réinitialisation de la base de données à chaque fois. Afin de contourner ce problème, nous allons ouvrir la connexion avant de la transmettre à EF Core et prévoir sa fermeture uniquement lorsque le test est terminé :

    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 l’amorçage des données.

L’exemple de code complet pour les tests en mémoire SQLite est disponible 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 plutôt d’utiliser SQLite ou d’implémenter le modèle de référentiel. Si vous avez décidé d’utiliser une ressource 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 est disponible ici.

Dénomination de la 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 plusieurs fois à la même base de données 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 gérées à partir du fournisseur de services interne du contexte. Par conséquent, 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 fait en ayant un champ statique InMemoryDatabaseRoot ).

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. Si vous souhaitez que les transactions soient ignorées en mode silencieux, vous pouvez configurer EF Core pour les ignorer InMemoryEventId.TransactionIgnoredWarning suivant 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.

Les vues

Le fournisseur en mémoire permet de définir des vues via des requêtes LINQ à l’aide ToInMemoryQueryde :

modelBuilder.Entity<UrlResource>()
    .ToInMemoryQuery(() => context.Blogs.Select(b => new UrlResource { Url = b.Url }));