Functional testing for ASP.NET Core API

Aujourd’hui nous allons voir comment implémenter des tests fonctionnels pour une API en .NET Core.

A quoi servent les tests fonctionnels ?

D’après la définition admise communément :

Les tests fonctionnels sont le processus par lequel nous déterminons si un logiciel agit conformément à des exigences prédéterminées. Il utilise des techniques de test de type boîte noire, dans lesquelles le testeur n'a aucune connaissance de la logique interne du système.

Pour le cas d’une Web API , nous devons préparer des requêtes pour lesquels nous attendons certains résultats , pour éviter les régressions et être surs qu’au fil des différents itérations ou évolutions de notre API , il n’y aura aucun changement non prévu.

Doit-on dépenser de l’énergie pour cela ?

Définitivement oui ! Cela nous permettra de valider que toutes nos API ont le comportement attendu dans les cas « Happy Path » comme dans les cas aux limites.

Si les spécifications fonctionnelles sont bien faites, elles devraient décrire tous les cas possibles comme les retours en cas d’exception , les cas nominaux etc

Notre API

Nous allons tout d’abord définir une API pour notre exemple.

Elle sera volontairement simple.

Cette API va simplement récupérer des livres et ses auteurs : (en .NET 6.0 minimal API)

app.MapGet("/BookSimpleDTO", (IBookRepository _bookRepository, IMapper mapper) =>
{

    var result = _bookRepository.GetBook(1);
    var destination = mapper.Map<BookModel, BookSimpleDTO>(result);
    return destination;
})
.WithName("GetBookSimpleDTO");

app.MapGet("/BookFullDTO", (IBookRepository _bookRepository, IMapper mapper) =>
{
    var result = _bookRepository.GetBook(1);
    var destination = mapper.Map<BookModel, BookFullDTO>(result);
    return destination;
})
.WithName("BookFullDTO");

app.MapGet("/GetBookFullDTOWithProject", (IBookRepository _bookRepository, IMapper mapper) =>
{
    var result = _bookRepository.GetBookQueryable(1);
    var destination = mapper.ProjectTo<BookFullDTO>(result);
    return destination;
})
.WithName("GetBookFullDTOWithProject");

J’ai volontairement mis des appels en base de données donc si vous utilisez mon projet, prévoyez un SQL Server Express avec une base nommée AutoMapper qui sera automatiquement remplie au démarrage de l’application.

A quoi vont ressembler nos tests fonctionnels ?

Ils vont prendre la forme d’un projet de tests unitaires dans lequel nous allons tester des cas fonctionnels et non plus des méthodes unitairement.

Pour cela je vais créer une projet de test unitaires avec XUnit.

Celui-ci ira instancier mon API .NET 6.0 pour faire des appels aux différents endpoints.

Malgré tout, nous ne souhaitons pas polluer notre base de données SQL donc nous allons utiliser une base de données en mémoire.

Pour celà, il faudra créer une classe dans notre projet de tests comme suite :

 public class FunctionalTestingApp : WebApplicationFactory<Program>
    {
        protected override IHost CreateHost(IHostBuilder builder)
        {
            var root = new InMemoryDatabaseRoot();

            builder.ConfigureServices(services =>
            {
                services.RemoveAll(typeof(DbContextOptions<DBContext>));

                services.AddDbContext<DBContext>(options =>
                    options.UseInMemoryDatabase("Testing", root));
            });
           
            return base.CreateHost(builder).SeedDataBase();
        }
    }
    public static class MigrationManager
    {
        public static IHost SeedDataBase(this IHost host)
        {
            using (var scope = host.Services.CreateScope())
            {
                using (var BookContext = scope.ServiceProvider.GetRequiredService<DBContext>())
                {
                    try
                    {
                        BookContext.Authors.Add(new AuthorModel
                        {
                            Id = 1,
                            FirstName = "Alexandre",
                            LastName = "Castro"

                        });
                        BookContext.Authors.Add(new AuthorModel
                        {
                            Id = 2,
                            FirstName = "Martin",
                            LastName = "Fowler"

                        });
                        BookContext.Authors.Add(new AuthorModel
                        {
                            Id = 3,
                            FirstName = "Gang",
                            LastName = "Of Four"

                        });
                        BookContext.Authors.Add(new AuthorModel
                        {
                            Id = 4,
                            FirstName = "Christophe",
                            LastName = "Mommer"

                        });

                        BookContext.Books.Add(new BookModel
                        {
                            BookId = 1,
                            BookTitle = "AutoMapper samples",
                            AuthorId = 1
                        });

                        BookContext.Books.Add(new BookModel
                        {
                            BookId = 2,
                            BookTitle = "Clean Code",
                            AuthorId = 2
                        });

                        BookContext.Books.Add(new BookModel
                        {
                            BookId = 3,
                            BookTitle = "Design patterns",
                            AuthorId = 3
                        });
                        BookContext.Books.Add(new BookModel
                        {
                            BookId = 4,
                            BookTitle = "Docker pour les développeurs .Net",
                            AuthorId = 4
                        });
                        BookContext.Books.Add(new BookModel
                        {
                            BookId = 5,
                            BookTitle = "Blazor :développement front end d'application web dynamiques",
                            AuthorId = 4
                        });

                        BookContext.SaveChanges();
                    }
                    catch (Exception ex)
                    {
                        //Log errors or do anything you think it's needed
                        throw;
                    }
                }
            }
            return host;
        }
    }

Nous pouvons voir que notre classe hérite de WebApplicationFactory<Program> qui permet de créer des instances d’applications Web à la volée à des fins de test si celle-ci est dans le même projet.

Program quant à lui est la classe Program de l’application que nous souhaitons tester.

Si comme moi vous êtes en .NET 6.0 , il vous faudra « exposer » votre classe Program comme suit dans le fichier Program.cs de l’application à tester :

public partial class Program
{
    // Expose the Program class for use with WebApplicationFactory<T>
}

Ici nous créons la base de données en mémoire et nous l’alimentons car des données à l’initialisation avec la méthode statique juste au dessous ==> SeedDataBase

Nous avons donc maintenant tout ce qu’il nous faut pour tester 😎

Je n’ai implémenté qu’un test pour vous montrer comment faire mais le principe reste le même.

Je vais donc tester que mon endpoint /BookSimpleDTO me renvoie bien un DTO de type BookSimpleDTO non nul et dont l’Id est 1.

        [Fact]
        public async void Test1()
        {
            await using var application = new FunctionalTestingApp();

            var client = application.CreateClient();
            var book = await client.GetFromJsonAsync<BookSimpleDTO>("/BookSimpleDTO");
            Assert.IsType<BookSimpleDTO>(book);
            Assert.NotEqual(null, book);
            Assert.Equal(1, book.BookId);
        }

Mon test est passé comme prévu mais vous pouvez imaginer appeler plusieurs api qui ajoutent , suppriment et modifient puis voir si le résultat attendu dans les spécifications est bien le bon ; ou bien tester les retours d’exception.

C’est tout pour ce tutorial en espérant qu’il vous aidera à tester fonctionellement vos applications 😉

Si vous souhaitez regarder le code que j’ai utilisé, vous pourrez le retrouver ici.

Have fun coding !