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