Tests unitaires avec xUnit
Notions théoriques
Un test unitaire vérifie qu'une petite unité de code (une méthode, une classe) produit le résultat attendu. Les tests automatisés permettent de détecter les régressions : une modification du code qui casse un comportement existant.
xUnit : le framework standard .NET
xUnit est le framework de tests recommandé pour .NET. Il est déjà utilisé par Microsoft dans ses propres projets .NET.
Pour démarrer, créez un projet de test :
dotnet new xunit -o MonProjet.Tests
cd MonProjet.Tests
dotnet add reference ../MonProjet/MonProjet.csproj
dotnet add package Moq
[Fact] — test simple
Un test marqué [Fact] s'exécute une seule fois avec des valeurs fixes.
using Xunit;
public class GuerrierTests
{
[Fact]
public void Attaquer_RetourneLaForce_SiAttaqueNormale()
{
// Arrange : préparer les données
var guerrier = new Guerrier { Nom = "Aragorn", Force = 80 };
guerrier.Strategie = new AttaqueNormale();
// Act : exécuter l'action testée
int degats = guerrier.Attaquer();
// Assert : vérifier le résultat
Assert.Equal(80, degats);
}
}
La convention de nommage Methode_Situation_ResultatAttendu rend les tests auto-documentés. En lisant le nom du test, on comprend immédiatement ce qui est testé et ce qui est attendu.
[Theory] + [InlineData] — tests paramétrés
Un test marqué [Theory] s'exécute plusieurs fois avec des données différentes, fournies par [InlineData].
[Theory]
[InlineData(80, 80)] // force 80 → dégâts 80
[InlineData(50, 50)] // force 50 → dégâts 50
[InlineData(0, 0)] // force 0 → dégâts 0
public void AttaqueNormale_RetourneToujours_LaForce(int force, int degatsAttendu)
{
var strategie = new AttaqueNormale();
int degats = strategie.CalculerDegats(force);
Assert.Equal(degatsAttendu, degats);
}
Assertions principales
| Assertion | Vérifie |
|---|---|
Assert.Equal(attendu, reel) | Égalité de valeurs |
Assert.NotEqual(nonAttendu, reel) | Inégalité |
Assert.NotNull(objet) | L'objet n'est pas null |
Assert.Null(objet) | L'objet est null |
Assert.True(condition) | La condition est vraie |
Assert.False(condition) | La condition est fausse |
Assert.Throws<T>(() => ...) | Le code lève l'exception T |
Assert.Contains(element, collection) | La collection contient l'élément |
[Fact]
public void PersonnageFactory_LanceException_SiTypeInconnu()
{
// Assert.Throws vérifie qu'une exception est bien levée
Assert.Throws<ArgumentException>(() =>
PersonnageFactory.Creer((TypePersonnage)99, "Test")
);
}
Moq : simuler les dépendances
Moq permet de créer des mocks (faux objets) pour simuler des dépendances (base de données, APIs) sans avoir besoin d'un vrai serveur.
using Moq;
public class PersonnageServiceTests
{
[Fact]
public void TrouverParId_RetournePersonnage_SiExiste()
{
// Arrange : créer un mock du repository
var mockRepo = new Mock<IPersonnageRepository>();
var guerrier = new Personnage { Id = 1, Nom = "Aragorn" };
// Configurer le comportement du mock
mockRepo.Setup(r => r.TrouverParId(1)).Returns(guerrier);
var service = new PersonnageService(mockRepo.Object);
// Act
var resultat = service.TrouverParId(1);
// Assert
Assert.NotNull(resultat);
Assert.Equal("Aragorn", resultat.Nom);
// Vérifier que la méthode a bien été appelée
mockRepo.Verify(r => r.TrouverParId(1), Times.Once);
}
}
Lancer les tests
dotnet test # Lancer tous les tests
dotnet test --filter "ClassName=X" # Filtrer par classe
dotnet test --collect:"XPlat Code Coverage" # Avec couverture (Coverlet)
Exemple pratique
// Fichier : GuerrierTests.cs
using Xunit;
using Moq;
// === Classes à tester (dans le projet principal) ===
public interface IPersonnageRepository
{
Personnage? TrouverParId(int id);
List<Personnage> TousLesPersonnages();
}
public class PersonnageService
{
private readonly IPersonnageRepository _repo;
public PersonnageService(IPersonnageRepository repo) => _repo = repo;
public Personnage? TrouverParId(int id) => _repo.TrouverParId(id);
}
// === Tests ===
public class AttaqueTests
{
[Fact]
public void AttaqueNormale_EgaleForce()
{
var strategie = new AttaqueNormale();
Assert.Equal(80, strategie.CalculerDegats(80));
}
[Fact]
public void AttaqueCritique_DoubleLesDegats()
{
var strategie = new AttaqueCritique();
Assert.Equal(160, strategie.CalculerDegats(80));
}
[Theory]
[InlineData(80, 80)]
[InlineData(50, 50)]
[InlineData(0, 0)]
public void AttaqueNormale_Toujours_EgaleForce(int force, int attendu)
{
var strategie = new AttaqueNormale();
Assert.Equal(attendu, strategie.CalculerDegats(force));
}
}
public class PersonnageServiceTests
{
[Fact]
public void TrouverParId_RetournePersonnage_SiExiste()
{
var mockRepo = new Mock<IPersonnageRepository>();
mockRepo.Setup(r => r.TrouverParId(1))
.Returns(new Personnage { Id = 1, Nom = "Aragorn" });
var service = new PersonnageService(mockRepo.Object);
var resultat = service.TrouverParId(1);
Assert.NotNull(resultat);
Assert.Equal("Aragorn", resultat!.Nom);
mockRepo.Verify(r => r.TrouverParId(1), Times.Once);
}
[Fact]
public void TrouverParId_RetourneNull_SiInexistant()
{
var mockRepo = new Mock<IPersonnageRepository>();
mockRepo.Setup(r => r.TrouverParId(It.IsAny<int>())).Returns((Personnage?)null);
var service = new PersonnageService(mockRepo.Object);
var resultat = service.TrouverParId(999);
Assert.Null(resultat);
}
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Dans ce TP, vous allez écrire des tests unitaires pour les classes Guerrier et PersonnageService de votre projet RPG.
Étape 1 — Tester la méthode Attaquer() de Guerrier
Écrivez un test [Fact] qui vérifie que Guerrier.Attaquer() retourne la valeur de Force avec une AttaqueNormale.
La séparation en trois blocs commentés (Arrange / Act / Assert) rend les tests lisibles et maintenables. "Arrange" prépare les données, "Act" exécute l'action testée (une seule), "Assert" vérifie le résultat. Si votre test a plusieurs "Act", c'est souvent le signe qu'il faut le découper en plusieurs tests.
Étape 2 — Tester avec [Theory] et [InlineData]
Réécrivez le test précédent sous forme de [Theory] pour couvrir plusieurs valeurs de Force.
Utiliser [Theory] avec [InlineData] au lieu de plusieurs [Fact] distincts évite la duplication de code de test. Si la logique du test change, vous ne modifiez qu'un seul endroit. Regroupez les cas normaux dans un Theory et réservez les [Fact] aux cas spéciaux (exception, valeur nulle, etc.).