Introduction : Pourquoi les tests ne sont pas un luxe, mais une nécessité
Dans le monde du développement moderne en C# (.NET), la qualité du code détermine non seulement la vitesse de mise sur le marché du produit, mais aussi sa réputation. Les erreurs en production, les plantages soudains après une mise à jour de dépendance ou le fonctionnement incorrect de la logique métier sont des scénarios qui peuvent être évités grâce à des tests bien conçus.
De nombreux développeurs, surtout les débutants, perçoivent les tests comme un "travail supplémentaire". Cependant, dans la pratique, les tests font gagner des heures de débogage et de refactorisation. Dans cet article, nous allons analyser les trois niveaux clés de tests dans l'écosystème C# : Unit (tests unitaires), Integration (tests d'intégration) et E2E (End-to-End). Vous verrez comment chacun résout ses propres problèmes, quels outils utiliser et comment écrire des tests propres et maintenables.
1. Tests unitaires : Le fondement de la qualité
Les tests unitaires vérifient des blocs de code isolés (généralement une méthode ou une classe) indépendamment des dépendances externes. En C#, c'est le niveau de test le plus rapide et le plus stable. L'objectif principal est de s'assurer que chaque fonction fonctionne correctement avec différentes données d'entrée.
Outils pour les tests unitaires en C#
Les frameworks les plus populaires pour .NET sont xUnit (recommandé par Microsoft) et NUnit. Pour simuler les dépendances, on utilise la bibliothèque Moq ou FakeItEasy intégré à .NET.
Exemple de test unitaire (xUnit + Moq)
Supposons que nous ayons un service de calcul de réduction qui dépend d'un dépôt d'utilisateurs :
public class DiscountService{ private readonly IUserRepository _userRepo;
public DiscountService(IUserRepository userRepo) { _userRepo = userRepo; }
public decimal CalculateDiscount(int userId, decimal orderTotal) { var user = _userRepo.GetById(userId); if (user.IsVip) return orderTotal * 0.2m; // 20% de réduction return orderTotal * 0.05m; // 5% de réduction }}Test pour cette méthode :
public class DiscountServiceTests{ [Fact] public void CalculateDiscount_WhenUserIsVip_Returns20Percent() { // Arrange var mockRepo = new Mock<IUserRepository>(); mockRepo.Setup(repo => repo.GetById(1)) .Returns(new User { Id = 1, IsVip = true }); var service = new DiscountService(mockRepo.Object);
// Act var result = service.CalculateDiscount(1, 100m);
// Assert Assert.Equal(20m, result); }}Important : Dans les tests unitaires, nous n'accédons pas à une base de données réelle. Nous utilisons un mock pour isoler la logique de DiscountService.
2. Tests d'intégration : Vérification de l'interaction des composants
Les tests d'intégration vérifient l'interaction de plusieurs composants réels : base de données, API, système de fichiers. Dans .NET, on utilise souvent WebApplicationFactory (pour ASP.NET Core) et des conteneurs de test (Testcontainers).
Caractéristiques des tests d'intégration
- Plus lents que les tests unitaires — nécessitent le déploiement d'une infrastructure (BDD, serveur HTTP).
- Plus coûteux à maintenir — il faut gérer l'état de l'environnement de test.
- Valeur ajoutée — ils révèlent des problèmes de configuration, de mappage de données, de fonctionnement avec une vraie base de données.
Exemple de test d'intégration (ASP.NET Core + xUnit)
Supposons que nous ayons un contrôleur pour gérer les commandes. Le test vérifiera que la requête à l'API réelle renvoie un JSON correct :
public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>{ private readonly HttpClient _client;
public OrdersApiTests(WebApplicationFactory<Program> factory) { _client = f