Comment créer des tests unitaires en C# ?
Note : publié initialement le 29/10/2021
Comme leur nom l'indique, les jeux de tests vont comporter un certain nombre de tests sur des méthodes d'une classe choisie.
Dans Visual Studio 2019, allez au niveau de votre solution, choisissez « Ajouter > Nouveau Projet », puis cherchez « Test » dans la liste des projets disponibles. Nous allons pour cet exemple utiliser MSTest pour .NET Core.

Après avoir nommé le projet (typiquement, un nom contenant le projet de base et avec le mot clef UnitTests ou Tests), Visual Studio a créé les fichiers pour les tests, avec un script UnitTest1.cs contenant les scripts de base ci-dessous et les fichiers de dépendances.
On voit qu'il importe par défaut un namespace UnitTesting servant aux tests unitaires.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace GameCodeClub.People.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
}
}
}
Il nous faut faire le lien entre le jeu de tests et la classe à tester. On fait un clic droit sur la section « Dépendances » du projet de test, on choisit « Ajouter une référence de projet… » et on coche ce qu'on veut tester.
Cela va automatiquement ajouter dans notre fichier de tests l'import l'espace de nom comprenant la classe que nous voulons tester, par exemple :
using GameCodeClub.People;
Le fichier de test doit être écrit ainsi :
- La classe de test indiquant ce qui est testé, précédée de
[TestClass] - Les méthodes de tests, avec un nom indiquant ce qui est testé et le résultat attendu (et éventuellement le nom de la méthode), chaque méthode doit être précédée de
[TestMethod] - Une méthode de Assert dans la classe de test, indiquant le résultat attendu (et éventuellement le message d'erreur à retourner).
Exemple de test avec Assert.IsTrue / Assert.IsFalse
Par exemple, Assert permet de tester si une valeur cible de type booléen doit être juste avec Assert.IsTrue ou fausse avec Assert.IsFalse. Ci-dessous, nous créons des instances de personnes de la classe Person et nous testons la propriété IsAdult de cette classe:
[TestMethod]
public void IsAdult_AgeLessThan18_ReturnFalse()
{
Person Person8YearsOld = new Person("Anna Test", 8);
Assert.IsFalse(Person8YearsOld.IsAdult, "Une personne de 8 ans n'est pas adulte.");
}
public void IsAdult_AgeOverThan18_ReturnTrue()
{
Person Person36YearsOld = new Person("Benjamin Test", 36);
Assert.IsTrue(Person36YearsOld.IsAdult, "Une personne de 36 ans est adulte");
}
public void IsAdult_AgeEqualsTo18_ReturnTrue()
{
Person Person18YearsOld = new Person("Charlie Test", 18);
Assert.IsTrue(Person18YearsOld.IsAdult, "Une personne de 18 ans est adulte");
}
Ainsi, si la propriété est correctement implémentée, elle devrait renvoyer False dans le 1er cas et True dans les deux suivants.
En faisant un clic droit sur notre projet de test et en choisissant « Exécuter les tests », Visual Studio ouvre l'Explorateur de Test et exécute tous les tests demandés. Cet explorateur indique quels tests ont réussi et quels test ont raté.
Après avoir lancé le test la première fois, Visual Studio indique directement dans l'interface où sont les erreurs.
Ci-dessous, j'ai volontairement mal codé la propriété IsAdult pour qu'elle ne fonctionne pas lorsque l'âge est exactement 18. En allant sur cette propriété après avoir exécuté les tests, j'ai le détail des tests réussis ou non.

Test des valeurs retournées par une méthode avec Assert.AreEqual()
Nous pouvons également tester des méthodes. Par exemple, si nous avons une méthode statique qui permet d'avoir les initiales à partir d'un nom (entrée et résultat tous deux de type string), nous allons pouvoir vouloir la tester sur certains cas extrêmes, voire sur une liste de cas, pour nous assurer qu'elle fonctionne toujours comme attendu.
Voici une méthode de test utilisant une liste de valeurs d'entrée dont les réponses attendues sont connues :
[TestMethod]
public void GetInitials_DataSet()
{
Dictionary<string, string> correctValues = new();
correctValues.Add("Alphonse Allais", "A. A.");
correctValues.Add("Jean-Jacques Rousseau", "J. J. R.");
correctValues.Add("Voltaire", "V.");
correctValues.Add("Victor Hugo", "V. H.");
correctValues.Add("George Sand", "G. S.");
foreach (KeyValuePair<string, string> name in correctValues)
{
Assert.AreEqual(Person.GetInitials(name.Key), name.Value);
}
}
Par exemple, si notre fonction est H.S. pour les noms composés, sur l'explorateur de tests, nous pourrons voir que « DataSet » retourne un échec. En cliquant pour voir le détail, nous pouvons voir sur quel élément du DataSet la fonction n'a pas marché.

Pour tester avec plus de précision, nous pouvons faire des méthodes de test précise pour certains comportements de la fonction. Ainsi nous savons qu'il y a des cas particuliers : prénom composé, présence d'une particule, ou nom unique. Nous faisons des méthodes explicites pour ces cas de figure :
[TestMethod]
public void GetInitials_NameWithHyphen_GetAllLetters()
{
Assert.AreNotEqual(Person.GetInitials("Jean-Jacques Rousseau"), "J. R.", "L'initiale du deuxième prénom n'est pas prise en compte dans l'implémentation.");
Assert.AreEqual(Person.GetInitials("Jean-Jacques Rousseau"), "J. J. R.");
}
[TestMethod]
public void GetInitials_OneName_GetOneLetters()
{
Assert.AreEqual(Person.GetInitials("Voltaire"), "V.");
}
[TestMethod]
public void GetInitials_NameWithParticle_IgnoreParticle()
{
Assert.AreEqual(Person.GetInitials("Honoré de Balzac"), "H. B.", "La particule doit être ignorée.");
Assert.AreNotEqual(Person.GetInitials("Honoré de Balzac"), "H. d. B.", "La particule doit être ignorée.");
}
Grâce à ces fonctions au nom et au tests explicites, on sait exactement ce qui n'a pas fonctionné et pourquoi cela n'a pas fonctionné.
N.B. : les classes sont redondantes à titre d'illustration, mais un seul AreEqual() ou AreNotEqual() est en l'occurrence amplement suffisant pour relever l'erreur de test.
Ci-dessous, on voit que notre implémentation sommaire de la méthode GetInitials() ne fonctionne que dans un nombre limités de cas. On va devoir améliorer la méthode jusqu'à ce que tout soit correct par rapport aux cas de tests, aussi bien les méthodes de test que le DataSet.

Le gros intérêt de ces méthodes de tests : ils permettent de vérifier le fonctionnement de classes et de méthodes hors de l'environnement.
Le Test Driven Development (Développement Piloté par les Tests) utilise les Tests comme point de départ. On écrit d'abord les tests, puis on créée le code de l'application, jusqu'à ce qu'il passe tous les tests.
Ce qu'il est possible de tester avec la classe Assert
Voici une petite référence de ce que contient Assert :
| Méthode de test | Méthode inverse | Effet |
|---|---|---|
AreEqual(a, b) | AreNotEqual(a, b) | Teste si les valeurs de a et b sont égales. |
AreSame(a, b) | AreNotSame(a, b) | Teste si a et b sont le même objet (la même instance) |
IsTrue(b) | IsFalse(b) | Teste si B (booléen) est vrai. |
IsNull(o) | IsNotNull(o) | Vérifie si l’objet o est null. |
IsInstanceOfType(o, T) | IsNotInstanceOfType(o, T) | Vérifie si l’objet o est une instance du type T. |
Fail(msg) | ... | Indique une erreur pour la méthode testée (avec le message facultatif msg) |
Inconclusive(msg) | ... | Indique que le test n’est ni validé, ni en erreur (avec un message faculatif msg); |
Assert.Fail() et Assert.Inconclusive() permettent notamment de faire des tests sur sur les comportements inattendus des fonctions (en les mettant dans un try/catch, par exemple).
Assert.Inconclusive() est traité de manière spécifique dans l'explorateur de tests, avec une icône « Warning ». Vous pouvez l'utiliser pour des cas où la vérification du test nécessite une ressource qui n'est pas disponible au moment du test, par exemple (même si la bonne pratique, pour avoir des tests efficace, c'est qu'ils soient indépendant d'autres ressources).
Vous pouvez aussi l'utiliser comme contenu par défaut des tests que vous n'avez pas encore écrit, comme ça vous le distinguez des tests réussis ou ratés.
Conclusion : les interfaces et tests rendent votre programme robuste et modulable
Nous l'avons vu, les interfaces comme les tests sont des outils très utiles au développement, notamment en équipe.
L'interface est plutôt pour l'architecture d'une application, vous permettre de vous assurer qu'une classe que vous ajoutez ou modifiez a toujours ce qu'il faut pour être utilisée par l'application. Et de même, si une nouveauté est ajoutée à une interface, le formalisme du code vous forcera à retoucher les classes concernées pour que tout fonctionne avec cette interface.
Les tests unitaires sont des outils très puissants pour vérifier le bon fonctionnement de votre application, aussi bien en amont du développement que lors des refactorisations de code ou ajout de nouvelles fonctions. La présence d'un jeu de tests précis et exhaustifs vous assure à toute étape que le programme fonctionne exactement comme attendu, et que le programme ne subit pas de régressions sur son fonctionnement attendu.