Introduction
En 2026, avec .NET 9 et les pipelines CI/CD omniprésents, NUnit reste le framework de test unitaire de référence pour les développeurs .NET intermédiaires. Contrairement à xUnit plus minimaliste, NUnit offre une syntaxe riche en attributs comme [TestCase] pour les données paramétrées, [SetUp]/[TearDown] pour l'initialisation, et une intégration native avec Visual Studio Test Explorer.
Ce tutoriel vous guide pas à pas pour créer un projet de tests robuste sur un service de calculatrice réel, en couvrant assertions avancées, mocking avec Moq, tests async, et configuration pour CI. Imaginez tester une API financière : un divide par zéro mal géré peut coûter cher ; NUnit vous protège avec 100% de couverture.
À la fin, vos tests seront exécutables via dotnet test, intégrables à GitHub Actions, et scalables pour des équipes. Prêt à booster la fiabilité de votre code ? (128 mots)
Prérequis
- .NET 8 SDK (ou supérieur, vérifiez avec
dotnet --version) - Visual Studio 2022 ou VS Code avec extension C# Dev Kit
- Connaissances de base en C# (classes, interfaces, async/await)
- Git installé pour la gestion de projet
- Outils de ligne de commande (PowerShell ou Bash)
Créer la solution et les projets
dotnet new sln -n CalculatorApp
dotnet new classlib -n CalculatorApp.Core
dotnet sln CalculatorApp.sln add CalculatorApp.Core/CalculatorApp.Core.csproj
dotnet new nunit -n CalculatorApp.Tests
dotnet sln CalculatorApp.sln add CalculatorApp.Tests/CalculatorApp.Tests.csproj
dotnet add CalculatorApp.Core/CalculatorApp.Core.csproj package MathNet.Numerics
dotnet add CalculatorApp.Tests/CalculatorApp.Tests.csproj package NUnit
dotnet add CalculatorApp.Tests/CalculatorApp.Tests.csproj package NUnit3TestAdapter
dotnet add CalculatorApp.Tests/CalculatorApp.Tests.csproj package Microsoft.NET.Test.Sdk
dotnet add CalculatorApp.Tests/CalculatorApp.Tests.csproj reference CalculatorApp.Core
dotnet buildCe script crée une solution avec un projet Core (bibliothèque de classes pour le service métier) et un projet Tests (template NUnit officiel). Il ajoute MathNet pour des calculs avancés, référence le Core dans Tests, et build pour vérifier. Évitez les templates manuels : le template nunit inclut déjà les packages essentiels, réduisant les erreurs de NuGet.
Implémenter le service à tester
using System;
using System.Threading.Tasks;
namespace CalculatorApp.Core;
public interface IMathService
{
double Add(double a, double b);
double Multiply(double a, double b);
double Divide(double a, double b);
Task<double> AsyncCompute(double a, double b);
}
public class MathService : IMathService
{
public double Add(double a, double b) => a + b;
public double Multiply(double a, double b) => a * b;
public double Divide(double a, double b)
{
if (b == 0)
throw new ArgumentException("Division par zéro interdite", nameof(b));
return a / b;
}
public async Task<double> AsyncCompute(double a, double b)
{
await Task.Delay(100); // Simulation async
return a + b;
}
}Ce service IMathService/MathService expose des méthodes synchrone (Add, Multiply, Divide avec exception) et async (AsyncCompute avec delay pour tester await). L'interface permet le mocking. Piège : toujours throw des exceptions nommées pour des asserts précis ; sans interface, le mocking est impossible.
Structure des tests basiques
Avec le service en place, les tests NUnit s'appuient sur [TestFixture] pour la classe, [Test] pour chaque méthode, et [SetUp] pour initialiser avant chaque test – comme un constructeur frais à chaque exécution, évitant les fuites d'état. Les assertions comme Assert.AreEqual ou Assert.Throws sont chainables et descriptives. Exécutez avec dotnet test : le runner NUnit affiche les échecs avec stack traces précises.
Tests basiques et assertions
using NUnit.Framework;
using CalculatorApp.Core;
namespace CalculatorApp.Tests;
[TestFixture]
public class MathServiceTests
{
private IMathService _service;
[SetUp]
public void SetUp()
{
_service = new MathService();
}
[Test]
public void Add_TwoPositiveNumbers_ReturnsSum()
{
// Act
var result = _service.Add(2, 3);
// Assert
Assert.AreEqual(5, result);
Assert.That(result, Is.EqualTo(5));
}
[Test]
public void Multiply_NegativeAndPositive_ReturnsNegative()
{
var result = _service.Multiply(-4, 2);
Assert.AreEqual(-8, result, "Produit doit être négatif");
}
[Test]
public void Divide_ByZero_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => _service.Divide(10, 0));
}
}Ces tests couvrent happy path (Add), edge case (Multiply négatif) et exception (Divide). [SetUp] réinstancie le service, comme un reset automatique. Utilisez Assert.That pour fluent API moderne ; le message custom dans AreEqual aide au debug. Copiez-collez : 100% coverage basique.
Tests paramétrés avec TestCase
Pour éviter la duplication, [TestCase] injecte des données multiples : idéal pour valider des combinaisons comme positives/négatifs/zéros. C'est plus performant que des boucles en code test, car NUnit parallelise. Analogie : comme un table-driven testing en Go, mais avec syntaxe attribut declarative.
Tests avec données paramétrées
using NUnit.Framework;
using CalculatorApp.Core;
namespace CalculatorApp.Tests;
[TestFixture]
public class MathServiceTests
{
private IMathService _service;
[SetUp]
public void SetUp()
{
_service = new MathService();
}
[TestCase(2, 3, 5)]
[TestCase(-1, 1, 0)]
[TestCase(0, 0, 0)]
public void Add_VariousInputs_ReturnsCorrectSum(double a, double b, double expected)
{
var result = _service.Add(a, b);
Assert.AreEqual(expected, result, 0.001); // Tolérance pour floats
}
[TestCase(10, 2, 5)]
[TestCase(10, -2, -5)]
public void Divide_ValidInputs_ReturnsQuotient(double a, double b, double expected)
{
var result = _service.Divide(a, b);
Assert.AreEqual(expected, result, 0.001);
}
[Test]
public void Divide_ByZero_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => _service.Divide(10, 0));
}
}Extension du fichier précédent : [TestCase] teste 3 scénarios Add en un test, avec tolérance 0.001 pour doubles imprécis. Ajoutez autant de cases que needed ; NUnit les exécute indépendamment. Piège : Oubliez la tolérance sur floats → faux négatifs.
Ajouter Moq pour mocking
dotnet add CalculatorApp.Tests/CalculatorApp.Tests.csproj package Moq
dotnet add CalculatorApp.Core/CalculatorApp.Core.csproj package Moq
dotnet buildMoq mocke les interfaces comme IMathService pour isoler les unités. Ajout en deux projets car mocks potentiellement partagés. Build valide les dépendances ; sans ça, les tests avec mocks échouent à la compilation.
Mocking et dépendances
À niveau intermédiaire, isolez les tests avec Moq : mockez IMathService pour tester un consommateur sans impl réelle. Setup définit comportements, Verify assert interactions. Parfait pour TDD sur services couplés.
Tests avec mocks et async
using NUnit.Framework;
using Moq;
using CalculatorApp.Core;
using System;
using System.Threading.Tasks;
namespace CalculatorApp.Tests;
[TestFixture]
public class MathServiceTests
{
private IMathService _service;
private Mock<IMathService> _mockService;
[SetUp]
public void SetUp()
{
_service = new MathService();
_mockService = new Mock<IMathService>();
}
[Test]
public async Task AsyncCompute_CalledOnce_ReturnsSum()
{
// Act
var result = await _service.AsyncCompute(2, 3);
// Assert
Assert.AreEqual(5, result);
}
[Test]
public void Consumer_UsesService_AddCalled()
{
// Arrange
_mockService.Setup(s => s.Add(It.IsAny<double>(), It.IsAny<double>())).Returns(42);
var consumer = new TestConsumer(_mockService.Object);
// Act
consumer.UseAdd();
// Assert
_mockService.Verify(s => s.Add(1, 2), Times.Once);
}
}
public class TestConsumer
{
private readonly IMathService _service;
public TestConsumer(IMathService service) => _service = service;
public void UseAdd() => _service.Add(1, 2);
}Ajout async test avec await et mock pour TestConsumer (dépendance injectée). It.IsAny match any args, Times.Once vérifie appel exact. Copier-collez exécutable ; isole les unités, accélère les tests x10 vs E2E.
Bonnes pratiques
- Un assert par test : Évite les cascades d'erreurs obscures ; chaque test = une intention claire.
- Nommez descriptivement :
Add_TwoPositives_ReturnsSum>Test1, pour debug rapide en CI. - Utilisez [OneTimeSetUp] pour setup lourd (DB mock), [SetUp] pour per-test.
- Couverture >80% : Visez avec
dotnet test --collect:"XPlat Code Coverage", intégrez à SonarQube. - Parallélisez : Ajoutez
dans .csproj pour x4 vitesse sur CI.
Erreurs courantes à éviter
- Oublier [SetUp] : État partagé → tests flaky ; toujours réinitialiser.
- Assert sans tolérance sur double/float :
AreEqual(0.1 + 0.2, 0.3)échoue (0.300000004 vs 0.3). - Mock non Verified : Couvre interactions manquées ; toujours
Verify()post-act. - Tests async sans await : Exceptions silencieuses ; utilisez
Assert.ThrowsAsync.
Pour aller plus loin
- Documentation officielle : NUnit GitHub
- Moq avancé : Moq Quickstart
- CI/CD : Intégrez à GitHub Actions avec
dotnet test --logger "trx" - Formations expertes : Découvrez nos formations .NET avancées chez Learni pour TDD et architecture clean.
- Outils : Coverlet pour coverage, FluentAssertions pour assertions plus lisibles.