Aller au contenu principal

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);
}
}
info

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

AssertionVé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


Quel attribut marque une méthode de test qui s'exécute une seule fois avec des valeurs fixes ?


Quel attribut, combiné à [InlineData], permet de lancer un test plusieurs fois avec des données différentes ?


Quelle assertion vérifie qu'une exception est levée par le code testé ?


À quoi sert Moq dans les tests unitaires ?


Quelle convention de nommage des tests est recommandée avec xUnit ?


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.


Bonne pratique - Respecter la structure Arrange / Act / Assert

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.


Bonne pratique - Préférer [Theory] quand on teste plusieurs valeurs du même comportement

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.).

Étape 3 — Tester PersonnageService avec un mock

Écrivez un test qui vérifie que PersonnageService.TrouverParId() retourne null quand le repository ne trouve rien.


Bonne pratique pour les matchers génériques

It.IsAny<int>() configure le mock pour répondre à n'importe quelle valeur entière. C'est utile pour tester un comportement général (ex: "retourner null si l'ID n'existe pas") sans lier le test à une valeur spécifique. Réservez les valeurs exactes (It.Is<int>(x => x == 1)) quand vous testez qu'une valeur particulière est bien traitée.

📌 Une solution