Comment tester les Minimal API

c sharp logo

Après avoir vu comment mettre en place des endpoints avec les Minimal API, il est normal de vouloir savoir comment les tester. Dans cet article on fait donc un retour sur la vidéo et on regarde comment tester les Minimal API. Bonne lecture!

Voici tout d’abord la vidéo publiée dernièrement sur le sujet:

Aussi, si vous n’aviez pas eu la chance de lire l’article et voir la vidéo sur les Minimal API, il est disponible here.

Différence avec les tests de controlleurs

La première chose qu’on peut remarquer est que les tests unitaires ne sont pas aussi facile qu’avec une méthode de controlleur. Prenons en exemple simplifié un GET pour aller chercher une liste de personnes:

public class PersonController : ControllerBase
{
    private readonly IPersonService personService;

    public PersonController(IPersonService personService) =>
        this.personService = personService;

    [HttpGet("api/person")]
    public IEnumerable<Person> GetAll() =>
        personService.GetPersons().Select(p => p.ToPerson());
}

Écrire des tests unitaires pour la méthode GetAll, en utilisant un mock for IPersonService est quelque chose de simple à faire. C’est un peu différent avec les endpoints qui sont construits via une méthode d’extension. Par exemple, l’équivalent de la méthode plus haut en Minimal API:

endpointRouteBuilder
    .MapGet(Routes.Person,
        (IPersonService personService) =>
            personService.GetPersons().Select(f => f.ToPerson()));

Donc pour tester ceci, on ne peut pas se rabattre sur des tests unitaires de la même manière qu’avec les controlleurs. On peut donc aller vers des tests d’intégrations, qui sont plutôt simple à mettre en place.

Préparation aux tests

Avant d’écrire nos tests, nous allons créer une fixture qui va s’occuper de contenir la création de notre application pour les tests. Elle va aussi nous exposer un client http, que nous utiliserons dans nos tests pour faire les appels aux endpoints.

public class MinimalApiApplicationFixture<TProgram> where TProgram : class
{
    public MinimalApiApplicationFixture() =>
        HttpClient = new AspNetMinimalApiApplication().CreateClient();

    public HttpClient HttpClient { get; }

    public async Task AuthenticateClient()
    {
        HttpResponseMessage loginResponse =
            await HttpClient.PostAsJsonAsync("/api/login", new UserLogin { Username = "bbarrette" });
        LoginResult? loginResult =
            await loginResponse.Content.ReadFromJsonAsync<LoginResult>();

        HttpClient.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", loginResult?.Token ?? string.Empty);
    }

    private class AspNetMinimalApiApplication : WebApplicationFactory<TProgram>
    {
        protected override IHost CreateHost(IHostBuilder builder) => base.CreateHost(builder);
    }
}

J’attire tout d’abord votre attention sur la classe AspNetMinimalApiApplication. C’est cette classe qui s’occupe de créer notre application et qui va donc injecter tout le nécessaire pour être fonctionnelle. Au niveau de la méthode CreateHost, c’est ici qu’on pourrait redéfinir certains éléments comme une connexion à la base de données. Si on revient au constructeur, on peut voir la création du client Http qui sera utilisé par nos tests. La méthode AuthenticateClient s’occupe d’aller chercher le token nécessaire pour être connecté à l’application, ce qui pourrait varier selon ce que vous utilisez. Comme notre fixture est publique, et que Program est interne, il faut aller faire un léger ajout dans la classe Program.cs pour y ajouter ceci:

#if DEBUG
namespace AspNetMinimalApi
{
    public partial class Program { }
}
#endif

Cet ajout va permettre à notre classe d’être publique uniquement dans un contexte de debug, ce qui est parfait pour nos tests.

Une classe de test par endpoint

Comme nos endpoints peuvent provenir d’un ou plusieurs fichiers, selon la séparation faite, j’aime bien y aller avec une classe de tests par endpoint. On peut donc concentrer le maximum d’initialisation commune à la construction et faire uniquement nos assertions dans les tests. Prenons par exemple les tests pour notre endpoint plus haut:

public class PersonGetTests : IClassFixture<MinimalApiApplicationFixture<Program>>, IAsyncLifetime
{
    private readonly MinimalApiApplicationFixture<Program> fixture;
    private HttpResponseMessage responseMessage = default!;

    public PersonGetTests(MinimalApiApplicationFixture<Program> fixture) =>
        this.fixture = fixture;

    public Task DisposeAsync() => Task.CompletedTask;

    public async Task InitializeAsync()
    {
        await fixture.AuthenticateClient();
        await fixture.HttpClient.PostAsJsonAsync("/api/person",
            new Person { Id = Guid.NewGuid(), FirstName = "Bruno", LastName = "Barrette" });

        responseMessage = await fixture.HttpClient.GetAsync("/api/person");
    }

    [Fact]
    public void Get_ShouldReturnOkResult() =>
        responseMessage.StatusCode.Should().Be(HttpStatusCode.OK);

    [Fact]
    public async Task Get_ShouldReturnTheListOfPerson() =>
        (await responseMessage.Content.ReadFromJsonAsync<Person[]>())!.Should().NotBeEmpty();
}

Au-delà de l’utilisation de la fixture, on peut remarquer que la classe implémente IAsyncLifetime. Cette interface nous amène à implémenter DisposeAsync and InitializeAsync. Cette dernière, qui nous intéresse, est appelée après le constructeur pour chaque test. On peut donc y faire toute initialisation asynchrone qui est nécessaire. Dans ce cas, on authentifie notre client, on s’assure d’avoir une personne dans le système, puis on fait notre appel au endpoint. Comme nous testons uniquement ce endpoint, on peut faire l’appel ici.

Avec ceci derrière nous, on peut concentrer nos tests à uniquement faire les vérifications nécessaires. Avec le format en expression, on s’assure de manière indirecte que notre test vérifie une seule et unique chose.

Couverture de code

Malgré que ce n’est pas le seul indicateur de la qualité du code, le pourcentage de couverture de code de nos tests peut aider à identifier des parties à risque. La bonne nouvelle est que nos tests d’intégrations aident à cette couverture. On peut donc adopter une stratégie différente, où on débute par nos tests d’intégrations qui couvrent les cas fréquents. Ensuite, on peut aller au niveau des services utilisés pour ajouter des tests unitaires pour certains cas spécifiques. En faisant ceci, non seulement notre couverture de code sera excellente, mais elle le sera avec moins de tests puisque les tests d’intégrations traversent toutes les couches applicatives.

Conclusion

On a donc vu que c’est plutôt simple mettre en place le nécessaire pour tester nos Minimal API. Au passage, ça nous amène une aussi bonne couverture de code, mais avec moins d’efforts et de tests. Si vous avez des questions ou commentaires suite à cet article, n’hésitez pas à me laisser savoir!

En rappel, les sources utilisées pour cet article et la vidéo: AspNetMinimalApi on GitHub

Bruno

Author: Bruno

By day, I am a developer, team leader and co-host of the Bracket Show. By evening, I am a husband and the father of two wonderful children. The time I have left after all of this I spend trying to get moving, playing and writing video game reviews, as well as preparing material for the Bracket Show recordings and for this blog. Through it all, I am also passionate about music and microbreweries.