Loupe

Mettre en place des tests d'intégrations (ASP.NET Core) - partie 2 - aller plus loin

Vous avez maintenant un projet de test d'intégration sur votre API en suivant la démarche que j'ai détaillée dans la partie 1. Pour rappel, dans les tests unitaires, l'approche est de tester des fonctionnalités (c'est à dire l'imbrication, enchaînement de code). Ces tests peuvent et doivent être éxecutés régulièrement pour garantir la conformité de l'application aux attentes et aux scénarios qui ont été définis. Il est donc nécessaire d'avoir des environnements éphémères lorsque les tests s'exécutent. Nous allons voir comment paramétrer notre environnement de test pour mieux répondre aux approches des tests unitaires en adaptant quelques briques de notre API.

Nous avions utilisé le WebHostBuilder de manière simple en utilisant uniquement la méthode UseStartup qui permet de configurer le builder en se reposant sur une classe Startup déjà créée. Nous allons nous pencher sur les différentes méthodes qui vont nous être très utiles.

Modifier les configurations de l'API

Dans une API ASP.NET Core, les configurations sont situées dans un fichier JSON à la racine de celle-ci nommé "appsettings". Dans ce fichier, vous pouvez mettre toutes les configurations nécessaires au bon fonctionnement de votre API, les chaînes de connexion aux bases de données, aux services de stockage, ... Il faut donc fournir des configurations de test à l'API, pour ce faire, il existe plusieurs manières.

La première est l'utilisation de fichier de configuration JSON (appsettings) dans le répertoire de test. Pour cela il faut utiliser la méthode UseContentRoot en spécifiant le dossier où se situe le-s fichier-s JSON de configuration.

var builder = WebHost.CreateDefaultBuilder()
                .UseContentRoot("Configuration");
arborescence-projet.PNG

Ainsi, la configuration utilisée pour mes tests sera celle située dans le dossier "Configuration". Si pour votre cas de test, il est nécessaire de spécifier un environnement d'exécution, il faut utiliser la méthode UseEnvironment en spécifiant le nom de l'environnement cible. Si je l'utilise en spécifiant "development", dans mon cas, la configuration chargée sera le fichier JSON "appsettings.Development.json". 

La seconde, est d'utiliser la méthode UseConfiguration qui permet de spécifier des configurations in-memory, sans fichier externe. Cette approche est plus rapide à mettre en place que les fichiers, et a l'avantage de vous permettre de centraliser dans la partie Arrange de vos tests toute votre configuration nécessaire au bon fonctionnement de votre cas de test.

Modifier les services injectés

Modifier les configurations peut être suffisant pour modifier le comportement d'un service. Mais pour aller plus loin, il est également possible de modifier les services injectés via l'injection de dépendances. Rien de plus simple, nous allons voir comment le configurer dans votre API pour que vos services soient facilement modifiables par la suite.

Côté API, il faut renseigner vos services dans la méthode ConfigureServices, avec les méthodes TryAddScoped, TryAddTransient, TryAddSingleton. Si vous partez d'une API déjà existante, il n'est pas difficile de remplacer les AddScoped, AddTransient, AddSingleton par ces nouvelles méthodes, le comportement sera semblable à ce que vous avez actuellement.

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.TryAddTransient<IAppService, AppService>();
    services.AddMvc();
}

Une fois les services renseignés de cette manière dans votre API, il suffit d'utiliser la méthode ConfigureServices proposée par le builder pour ainsi renseigner l'instance que vous souhaitez utiliser dans vos tests. La méthode ConfigureServices est appelée avant celle présente dans le Startup, votre service sera donc déjà présent dans le dictionnaire de dépendances, et ne sera pas écrasé par la vraie implémentation présente dans votre API.

// Arrange
var builder = WebHost.CreateDefaultBuilder();
var server = new TestServer(builder
     .ConfigureServices((services) =>
     {
          services.AddTransient<IAppService, TestAppService>();
     })
     .UseStartup<Startup>());

Maintenant que vous savez remplacer vos services, vous allez pouvoir profiter pleinement du framework ASP.NET Core et de ses possibilités, en remplaçant certaines briques de votre API par des briques de test, en utilisant par exemple, des frameworks de mock (MOQ) pour remplacer des objets par vos instances de test.

Utiliser Entity Framework Core In Memory

Si vous utilisez Entity Framework Core dans votre API, et que vous souhaitez utiliser des données de tests, vous n'allez pas regretter d'avoir fait le choix d'EF Core. Si vous n'avez pas encore fait ce choix, je vous le conseille vivement, il est très "test-friendly" ! Avec les anciennes versions d'entity framework, on pouvait simplement modifier la chaîne de connexion à notre base de donnée pour pointer sur une base de test. Il y avait plusieurs limitations a ce fonctionnement (coût d'infrastructure, temps de mise en place de la base - preparation et nettoyage des données -, ...). Il était donc parfois compliqué d'avoir un environnement de test adapté, sans y avoir consacré beaucoup de temps, ou d'argent.

Avec Entity Framework Core, vous pouvez, à des fin de tests, utiliser le provider in-memory, et cela très simplement ! Il est nécéssaire d'ajouter le package suivant : 

Install-Package Microsoft.EntityFrameworkCore.InMemory

Une fois ce package ajouté, vous pouvez configurer les options de votre contexte Entity Framework et utiliser la méthode UseInMemoryDatabase. Cette méthode prend en paramètre, le nom de votre base de données. Il faut bien choisir ce nom, si vous souhaitez utiliser et partager les mêmes données sur tous vos tests, utilisez le même nom, sinon vous pouvez donner un nom aléatoire. En combinant ceci avec ce que nous avons appris sur la modification des services injectés, je peux modifier le fonctionnement de mon contexte pour qu'il utilise un fonctionnement in-memory.

[Fact(DisplayName = "J'appelle la route companies sur l'API, elle me renvoit une liste des entreprises en base")]
public async Task GetCompanies_ShouldBeOk()
{
    // Arrange
    var options = new DbContextOptionsBuilder<ConfigContext>()
                .UseInMemoryDatabase(Guid.NewGuid().ToString())
                .Options;
    var builder = WebHost.CreateDefaultBuilder();
    var server = new TestServer(builder
        .ConfigureServices((services) =>
        {
            services.AddTransient<IConfigContext, ConfigContext>(_ => new ConfigContext(options));
        })
        .UseStartup<Startup>());
    var client = server.CreateClient();
    var expectedCompanies = new List<Company>
        {
            new Company
            {
                Name = "Infinite Square"
            },
            new Company
            {
                Name = "Microsoft"
            }
        };

    using (var context = new ConfigContext(options))
    {
        context.Companies.AddRange(expectedCompanies);
        await context.SaveChangesAsync();
    }

    // Act
    var httpResponseMessage = await client.GetAsync("api/companies");

    // Assert
    Assert.True(httpResponseMessage.IsSuccessStatusCode);
    var companies = JsonConvert.DeserializeObject<Company[]>(await httpResponseMessage.Content.ReadAsStringAsync());
    Assert.Equal(expectedCompanies.Count, companies.Length);
    Assert.Equal(expectedCompanies, companies);
}

Dans mon exemple, je configure mon contexte en in-memory avec la méthode UseInMemoryDatabase, je remplace pour mes tests, l'instance dans l'API avec une instance utilisant mes options, puis j'insère mes données de test dans mon contexte. Je fais un appel à l'API, et comme elle utilise le même contexte (via les options, et le nom de la base) je récupère les mêmes données.

Un autre avantage au mode in-memory d'entity framework, c'est la possibilité de voir toute les données dans le contexte depuis le débugger de Visual Studio. Ceci va vous permettre de mieux débugger certaines méthodes qui doivent écrire dans plusieurs tables de votre base données, voir même dans plusieurs bases de données à la fois !

debugger ef core in memory.gif

Attention, il est important de connaître les quelques limitations d'entity framework in-memory :

  • On ne peut pas gérer la collation, c'est a dire que si votre provider réel est case insensitive, vous ne pourrez pas reproduire le même comportement en in-memory. Il faut donc y penser dans votre code, en utilisant notamment des méthodes de comparaison qui ne sont case insensitive.
  • Dans certains cas, les contraintes d'intégrités ne sont pas contrôlées,
  • ...

Vous avez donc tous les outils en main pour réaliser des tests d'intégration sur votre API et ainsi la rendre robuste et facilement maintenable/évolutive. Les tests sont réellement un atout pour votre application, et ne doivent pas être négligés lors de la phase de développement, ils doivent être compris dans le chiffrage du développement de la fonctionnalité.

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus