Introduction : Pourquoi tester en C# ?
\nDans le développement moderne en C# (.NET), la qualité du code n'est pas un luxe, mais une nécessité. Les tests permettent non seulement de détecter les bugs avant la mise en production, mais servent également de documentation vivante et accélèrent le refactoring. Il existe trois niveaux principaux de tests qui forment la « pyramide des tests » : Unit (unitaires), Integration (d'intégration) et E2E (de bout en bout). Dans cet article, nous analyserons chacun d'eux avec des exemples de code réel en C#.
\n\n1. Tests unitaires : Le fondement de la qualité
\nLes tests unitaires vérifient un fragment de code isolé (méthode, classe) sans dépendances externes (base de données, système de fichiers, API). Dans l'écosystème .NET, trois frameworks sont populaires : xUnit (recommandé pour les nouveaux projets), NUnit et MSTest.
\n\nPrincipes fondamentaux (FIRST)
\n- \n
- Fast — rapides (millisecondes). \n
- Isolated — isolés (utilisation de mocks). \n
- Repeatable — reproductibles. \n
- Self-validating — auto-validants (Assert). \n
- Timely — opportuns. \n
Exemple de test unitaire avec xUnit et Moq
\nSupposons que nous ayons un service pour calculer une remise :
\npublic interface IDiscountService\n{\n decimal CalculateDiscount(decimal price, bool isLoyaltyMember);\n}\n\npublic class DiscountService : IDiscountService\n{\n public decimal CalculateDiscount(decimal price, bool isLoyaltyMember)\n {\n if (isLoyaltyMember) return price * 0.9m; // 10% de remise\n return price;\n }\n}\nTest utilisant Moq (pour simuler les dépendances) et xUnit :
\nusing Xunit;\nusing Moq;\n\npublic class DiscountServiceTests\n{\n [Fact]\n public void CalculateDiscount_WhenLoyaltyMember_ReturnsDiscountedPrice()\n {\n // Arrange\n var service = new DiscountService();\n var price = 100m;\n\n // Act\n var result = service.CalculateDiscount(price, true);\n\n // Assert\n Assert.Equal(90m, result);\n }\n\n [Theory]\n [InlineData(100, false, 100)]\n [InlineData(200, true, 180)]\n public void CalculateDiscount_MultipleCases_ReturnsCorrectValues(decimal price, bool isLoyalty, decimal expected)\n {\n var service = new DiscountService();\n var result = service.CalculateDiscount(price, isLoyalty);\n Assert.Equal(expected, result);\n }\n}\nConseil : Utilisez l'attribut [Fact] pour les tests uniques et [Theory] + [InlineData] pour les tests paramétrés.
2. Tests d'intégration : Vérification de l'interconnexion des composants
\nLes tests d'intégration vérifient comment plusieurs modules fonctionnent ensemble — par exemple, un contrôleur, un service et une base de données réelle (ou son remplacement, comme SQLite in-memory). Dans ASP.NET Core, on utilise WebApplicationFactory pour cela.
Configuration de WebApplicationFactory
\nCréons une classe de test qui démarre un hôte web de test :
\nusing Microsoft.AspNetCore.Mvc.Testing;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing MyApp.Data;\n\npublic class CustomWebApplicationFactory : WebApplicationFactory<Program>\n{\n protected override void ConfigureWebHost(IWebHostBuilder builder)\n {\n builder.ConfigureServices(services =>\n {\n // Supprimer le DbContext réel\n var descriptor = services.SingleOrDefault(\n d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));\n if (descriptor != null) services.Remove(descriptor);\n\n // Ajouter une base de données in-memory\n services.AddDbContext<AppDbContext>(options =>\n