Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Il existe de nombreux avantages de l’écriture de tests unitaires. Ils aident à la régression, fournissent de la documentation et facilitent une bonne conception. Toutefois, lorsque les tests unitaires sont difficiles à lire et fragiles, ils peuvent entraîner des dégâts sur votre base de code. Cet article décrit certaines bonnes pratiques pour la conception de tests unitaires pour prendre en charge vos projets .NET Core et .NET Standard. Vous apprenez des techniques pour maintenir vos tests résilients et faciles à comprendre.
Avec des remerciements particuliers à John Reese et Roy Osherove
Avantages des tests unitaires
Les sections suivantes décrivent plusieurs raisons d’écrire des tests unitaires pour vos projets .NET Core et .NET Standard.
Moins de temps d’exécution de tests fonctionnels
Les tests fonctionnels sont coûteux. Ils impliquent généralement l’ouverture de l’application et l’exécution d’une série d’étapes que vous (ou quelqu’un d’autre) devez suivre pour valider le comportement attendu. Ces étapes peuvent ne pas toujours être connues du testeur. Ils doivent contacter quelqu’un plus compétent dans le domaine pour effectuer le test. Le test lui-même peut prendre quelques secondes pour les modifications triviales ou les minutes pour les modifications plus importantes. Enfin, ce processus doit être répété pour chaque modification que vous apportez dans le système. Les tests unitaires, d’autre part, prennent des millisecondes, peuvent être exécutés à l’appui d’un bouton, et ne nécessitent pas nécessairement de connaissances du système en grande partie. C'est l'exécutant du test qui détermine si le test réussit ou échoue, et non l'individu.
Protection contre la régression
Les défauts de régression sont des erreurs introduites lorsqu’une modification est apportée à l’application. Il est courant que les testeurs testent non seulement leur nouvelle fonctionnalité, mais aussi les fonctionnalités de test qui existaient au préalable pour vérifier que les fonctionnalités existantes fonctionnent toujours comme prévu. Avec les tests unitaires, vous pouvez réexécuter toute votre suite de tests après chaque build ou même après avoir modifié une ligne de code. Cette approche permet d’accroître la confiance que votre nouveau code n’interrompt pas les fonctionnalités existantes.
Documentation exécutable
Il peut ne pas toujours être évident ce qu’une méthode particulière fait ou comment elle se comporte en fonction d’une certaine entrée. Vous pouvez vous demander : Comment cette méthode se comporte-t-elle si je lui transmet une chaîne vide ou null ? Lorsque vous disposez d’une suite de tests unitaires bien nommés, chaque test doit clairement expliquer la sortie attendue d’une entrée donnée. En outre, le test doit être en mesure de vérifier qu’il fonctionne réellement.
Code moins couplé
Lorsque le code est étroitement couplé, il peut être difficile de tester unitairement. Sans créer de tests unitaires pour le code que vous écrivez, le couplage peut être moins apparent. L’écriture de tests pour votre code découple naturellement votre code, car il est plus difficile de tester autrement.
Caractéristiques de bons tests unitaires
Il existe plusieurs caractéristiques importantes qui définissent un bon test unitaire :
- Fast: il n’est pas rare que les projets matures aient des milliers de tests unitaires. Les tests unitaires doivent durer très peu de temps. Millisecondes.
- isolé : les tests unitaires sont autonomes, peuvent s’exécuter en isolation et n’ont aucune dépendance sur des facteurs externes, tels qu’un système de fichiers ou une base de données.
- : reproductible L'exécution d'un test unitaire doit donner des résultats cohérents. Le test retourne toujours le même résultat si vous ne modifiez rien entre les exécutions.
- Auto-vérification: le test doit détecter automatiquement s’il a réussi ou échoué sans aucune interaction humaine.
- Rapide : L'écriture d'un test unitaire ne doit pas prendre un temps disproportionné par rapport au code testé. Si vous découvrez que le test du code prend beaucoup de temps par rapport à l’écriture du code, envisagez une conception plus testable.
Couverture du code et qualité du code
Un pourcentage élevé de couverture du code est souvent associé à une qualité de code plus élevée. Toutefois, la mesure elle-même ne peut pas déterminer la qualité du code. La définition d’un objectif de pourcentage de couverture de code trop ambitieux peut être contre-productif. Considérons un projet complexe comportant des milliers de branches conditionnelles, et supposons que vous vous fixiez un objectif de couverture de code de 95 %. Actuellement, la couverture du code du projet est de 90 %. Le temps nécessaire pour tenir compte de tous les cas limites dans les 5% restants peut représenter un travail colossal, et la proposition de valeur s'en trouve rapidement diminuée.
Un pourcentage de couverture de code élevé n’est pas un indicateur de réussite et n’implique pas une qualité de code élevée. Il représente simplement la quantité de code couverte par les tests unitaires. Pour plus d’informations, consultez la couverture du code de test unitaire.
Terminologie des tests unitaires
Plusieurs termes sont fréquemment utilisés dans le contexte des tests unitaires : fake, mock et stub. Malheureusement, ces termes peuvent être mal utilisés. Il est donc important de comprendre l’utilisation correcte.
Fake: Un fake est un terme générique qui peut être utilisé pour décrire soit un stub soit un objet fictif. Si l’objet est un stub ou une simulation dépend du contexte dans lequel l’objet est utilisé. En d'autres termes, un fake peut être un stub ou un fictif.
Mock: un objet fictif est un faux objet dans le système qui détermine si un test unitaire réussit ou échoue. Un fictif commence comme un faux et reste un faux jusqu'à ce qu'il entre dans une opération
Assert
.Stub: un stub est un remplacement contrôlable d’une dépendance existante (ou collaborateur) dans le système. En utilisant un stub, vous pouvez tester votre code sans gérer directement la dépendance. Par défaut, un stub commence comme un fake.
Considérez le code suivant :
var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
Ce code montre un stub appelé fictif. Mais dans ce scénario, le stub est vraiment un stub. L’objectif du code est de passer l’ordre comme moyen d’instancier l’objet Purchase
(le système sous test). Le nom de la classe MockOrder
est trompeur car la commande est un stub et non un fictif.
Le code suivant montre une conception plus précise :
var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);
purchase.ValidateOrders();
Assert.True(purchase.CanBeShipped);
Lorsque la classe est renommée en FakeOrder
, la classe est plus générique. La classe peut être utilisée comme fictif ou comme stub, selon les exigences du cas de test. Dans le premier exemple, la classe FakeOrder
est utilisée comme stub et n’est pas utilisée pendant l’opération de Assert
. Le code transmet la classe FakeOrder
à la classe Purchase
simplement pour répondre aux exigences du constructeur.
Pour utiliser la classe en tant que simulation, vous pouvez mettre à jour le code :
var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);
purchase.ValidateOrders();
Assert.True(mockOrder.Validated);
Dans cette conception, le code vérifie une propriété sur le faux (asserting against it), et par conséquent, la classe mockOrder
est un fictif.
Importante
Il est important d’implémenter correctement la terminologie. Si vous appelez vos stubs « fictifs », d’autres développeurs vont faire de fausses hypothèses sur votre intention.
La principale chose à retenir à propos des mocks par rapport aux stubs est que les mocks sont exactement comme les stubs, à l'exception du processus Assert
. Vous exécutez des opérations Assert
sur un objet fictif, mais pas sur un stub.
Meilleures pratiques
Il existe plusieurs bonnes pratiques importantes à suivre lors de l’écriture de tests unitaires. Les sections suivantes fournissent des exemples qui montrent comment appliquer les meilleures pratiques à votre code.
Éviter les dépendances d’infrastructure
Essayez de ne pas introduire de dépendances sur l’infrastructure lors de l’écriture de tests unitaires. Les dépendances rendent les tests lents et fragiles et doivent être réservés aux tests d’intégration. Vous pouvez éviter ces dépendances dans votre application en suivant le principe de dépendances explicites et en utilisant l'injection de dépendances dans .NET . Vous pouvez également conserver vos tests unitaires dans un projet distinct de vos tests d’intégration. Cette approche garantit que votre projet de test unitaire n’a pas de références ni de dépendances sur les packages d’infrastructure.
Respectez les normes de dénomination des tests
Le nom de votre test doit se composer de trois parties :
- Nom de la méthode testée
- Scénario dans lequel la méthode est testée
- Comportement attendu lorsque le scénario est appelé
Les normes d’affectation de noms sont importantes, car elles aident à exprimer l’objectif de test et l’application. Les tests sont plus que de s’assurer que votre code fonctionne. Ils fournissent également de la documentation. En examinant la suite de tests unitaires, vous devez être en mesure de déduire le comportement de votre code et de ne pas avoir à examiner le code lui-même. De plus, lorsque les tests échouent, vous pouvez voir exactement quels scénarios ne répondent pas à vos attentes.
Code original
[Fact]
public void Test_Single()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Appliquer les meilleures pratiques
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Organiser vos tests
Le modèle « Arranger, Agir, Affirmer » est une approche courante pour l’écriture de tests unitaires. Comme le nom l’indique, le modèle se compose de trois tâches principales :
- organiser vos objets, les créer et les configurer selon les besoins
- Agir sur un objet
- Assert que quelque chose est comme prévu
Lorsque vous suivez le modèle, vous pouvez clairement séparer ce qui est testé à partir des tâches Arrange et Assert. Ce modèle permet également de réduire la possibilité que les assertions s'entremêlent avec le code dans la tâche Act.
La lisibilité est l’un des aspects les plus importants lors de l’écriture d’un test unitaire. La séparation de chaque action de modèle dans le test met clairement en évidence les dépendances requises pour appeler votre code, la façon dont votre code est appelé et ce que vous essayez d’affirmer. Bien qu’il soit possible de combiner certaines étapes et de réduire la taille de votre test, l’objectif global est de rendre le test aussi lisible que possible.
Code original
[Fact]
public void Add_EmptyString_ReturnsZero()
{
// Arrange
var stringCalculator = new StringCalculator();
// Assert
Assert.Equal(0, stringCalculator.Add(""));
}
Appliquer les meilleures pratiques
[Fact]
public void Add_EmptyString_ReturnsZero()
{
// Arrange
var stringCalculator = new StringCalculator();
// Act
var actual = stringCalculator.Add("");
// Assert
Assert.Equal(0, actual);
}
Écrire des tests concluants minimaux
L’entrée d’un test unitaire doit être les informations les plus simples nécessaires pour vérifier le comportement que vous testez actuellement. L’approche minimaliste permet aux tests de devenir plus résilients aux modifications futures dans le codebase et de se concentrer sur la vérification du comportement sur l’implémentation.
Les tests qui incluent plus d’informations que nécessaire pour réussir le test actuel ont une probabilité plus élevée d’introduire des erreurs dans le test et peuvent rendre l’intention du test moins clair. Lors de l’écriture de tests, vous souhaitez vous concentrer sur le comportement. La définition de propriétés supplémentaires sur des modèles ou l’utilisation de valeurs non nulles si ce n’est pas obligatoire, ne supprime que ce que vous essayez de confirmer.
Code original
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("42");
Assert.Equal(42, actual);
}
Appliquer les meilleures pratiques
[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add("0");
Assert.Equal(0, actual);
}
Éviter les chaînes magiques
Les chaînes magiques sont des valeurs de chaîne codées en dur directement dans vos tests unitaires sans commentaire de code ni contexte supplémentaire. Ces valeurs rendent votre code moins lisible et plus difficile à gérer. Les chaînes magiques peuvent provoquer une confusion au lecteur de vos tests. Si une chaîne semble inhabituelle, ils peuvent se demander pourquoi on a choisi une certaine valeur pour un paramètre ou comme valeur de retour. Ce type de valeur de chaîne peut les amener à examiner de plus près les détails de l’implémentation, plutôt que de se concentrer sur le test.
Conseil / Astuce
Rendez votre objectif d’exprimer autant d’intention que possible dans votre code de test unitaire. Au lieu d’utiliser des chaînes magiques, affectez toutes les valeurs codées en dur aux constantes.
Code original
[Fact]
public void Add_BigNumber_ThrowsException()
{
var stringCalculator = new StringCalculator();
Action actual = () => stringCalculator.Add("1001");
Assert.Throws<OverflowException>(actual);
}
Appliquer les meilleures pratiques
[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
var stringCalculator = new StringCalculator();
const string MAXIMUM_RESULT = "1001";
Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);
Assert.Throws<OverflowException>(actual);
}
Éviter la logique de codage dans les tests unitaires
Lorsque vous écrivez vos tests unitaires, évitez la concaténation manuelle des chaînes, les conditions logiques, telles que if
, while
, for
et switch
et d’autres conditions. Si vous incluez une logique dans votre suite de tests, la possibilité d’introduire des bogues augmente considérablement. Le dernier endroit où vous souhaitez trouver un bogue se trouve dans votre suite de tests. Vous devez avoir un niveau élevé de confiance que vos tests fonctionnent, sinon, vous ne pouvez pas les approuver. Les tests en lesquels vous n'avez pas confiance ne fournissent aucune valeur. Lorsqu’un test échoue, vous souhaitez avoir un sentiment que quelque chose est incorrect avec votre code et qu’il ne peut pas être ignoré.
Conseil / Astuce
Si l’ajout de la logique dans votre test semble inévitable, envisagez de fractionner le test en deux tests ou plus différents pour limiter les exigences logiques.
Code original
[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
var stringCalculator = new StringCalculator();
var expected = 0;
var testCases = new[]
{
"0,0,0",
"0,1,2",
"1,2,3"
};
foreach (var test in testCases)
{
Assert.Equal(expected, stringCalculator.Add(test));
expected += 3;
}
}
Appliquer les meilleures pratiques
[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
var stringCalculator = new StringCalculator();
var actual = stringCalculator.Add(input);
Assert.Equal(expected, actual);
}
Utilisez des méthodes d’assistance au lieu de Configuration et de Démontage.
Si vous avez besoin d’un objet ou d’un état similaire pour vos tests, utilisez une méthode d’assistance plutôt que Setup
et Teardown
attributs, s’ils existent. Les méthodes d’assistance sont préférées à ces attributs pour plusieurs raisons :
- Moins de confusion lors de la lecture des tests, car tout le code est visible à partir de chaque test
- Moins de chances de configurer trop ou trop peu pour le test donné
- Moins de chances de partager l’état entre les tests, ce qui crée des dépendances indésirables entre eux
Dans les frameworks de test unitaire, l’attribut Setup
est appelé avant chaque test unitaire au sein de votre suite de tests. Certains programmeurs voient ce comportement comme utile, mais cela entraîne souvent des tests gonflés et difficiles à lire. Chaque test a généralement des exigences différentes pour l’installation et l’exécution. Malheureusement, l’attribut Setup
vous oblige à utiliser exactement les mêmes exigences pour chaque test.
Remarque
Les attributs SetUp
et TearDown
sont supprimés dans xUnit version 2.x et ultérieure.
Code original
Appliquer les meilleures pratiques
private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
stringCalculator = new StringCalculator();
}
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
var stringCalculator = CreateDefaultStringCalculator();
var actual = stringCalculator.Add("0,1");
Assert.Equal(1, actual);
}
// More tests...
// More tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
var result = stringCalculator.Add("0,1");
Assert.Equal(1, result);
}
private StringCalculator CreateDefaultStringCalculator()
{
return new StringCalculator();
}
Évitez les tâches Act multiples
Lorsque vous écrivez vos tests, essayez de n'inclure qu'une seule tâche Act par test. Certaines approches courantes pour mettre en œuvre une seule tâche Act consistent à créer un test distinct pour chaque Act ou à utiliser des tests paramétrés. Il existe plusieurs avantages à utiliser une seule tâche Act pour chaque test :
- Vous pouvez facilement déterminer quelle tâche Act échoue si le test échoue.
- Vous pouvez vous assurer que le test se concentre uniquement sur un seul cas.
- Vous obtenez une image claire de la raison pour laquelle vos tests échouent.
Les tâches Act multiples doivent être assertées individuellement, et vous ne pouvez pas garantir que toutes les tâches Assert s'exécutent. Dans la plupart des frameworks de test unitaire, une fois qu’une tâche Assert échoue dans un test unitaire, tous les tests suivants sont automatiquement considérés comme ayant échoué. Le processus peut prêter à confusion, car certaines fonctionnalités de travail peuvent être interprétées comme ayant échoué.
Code original
[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
// Act
var actual1 = stringCalculator.Add("");
var actual2 = stringCalculator.Add(",");
// Assert
Assert.Equal(0, actual1);
Assert.Equal(0, actual2);
}
Appliquer les meilleures pratiques
[Theory]
[InlineData("", 0)]
[InlineData(",", 0)]
public void Add_EmptyEntries_ShouldBeTreatedAsZero(string input, int expected)
{
// Arrange
var stringCalculator = new StringCalculator();
// Act
var actual = stringCalculator.Add(input);
// Assert
Assert.Equal(expected, actual);
}
Valider des méthodes privées avec des méthodes publiques
Dans la plupart des cas, vous n’avez pas besoin de tester une méthode privée dans votre code. Les méthodes privées sont un détail d’implémentation et n’existent jamais en isolation. À un moment donné dans le processus de développement, vous introduisez une méthode publique pour appeler la méthode privée dans le cadre de son implémentation. Lorsque vous écrivez vos tests unitaires, ce qui vous importe est le résultat final de la méthode publique qui fait appel à la méthode privée.
Considérez le scénario de code suivant :
public string ParseLogLine(string input)
{
var sanitizedInput = TrimInput(input);
return sanitizedInput;
}
private string TrimInput(string input)
{
return input.Trim();
}
En termes de test, votre première réaction peut être d’écrire un test pour la méthode TrimInput
pour s’assurer qu’elle fonctionne comme prévu. Toutefois, il est possible que la méthode ParseLogLine
manipule l’objet sanitizedInput
d’une manière que vous ne vous attendez pas. Le comportement inconnu peut rendre votre test sur la méthode TrimInput
inutile.
Dans ce scénario, un meilleur test consiste à vérifier la méthode publique ParseLogLine
:
public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
var parser = new Parser();
var result = parser.ParseLogLine(" a ");
Assert.Equals("a", result);
}
Lorsque vous rencontrez une méthode privée, recherchez la méthode publique qui appelle la méthode privée et écrivez vos tests sur la méthode publique. Simplement parce qu’une méthode privée retourne un résultat attendu, ne signifie pas que le système qui appelle finalement la méthode privée utilise correctement le résultat.
Gérer les références statiques des stub avec des coutures
L’un des principes d’un test unitaire est qu’il doit avoir un contrôle total du système testé. Toutefois, ce principe peut être problématique lorsque le code de production inclut des appels à des références statiques (par exemple, DateTime.Now
).
Examinez le scénario de code suivant :
public int GetDiscountedPrice(int price)
{
if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
Pouvez-vous écrire un test unitaire pour ce code ? Vous pouvez essayer d’exécuter une tâche Assert sur le price
:
public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
var priceCalculator = new PriceCalculator();
var actual = priceCalculator.GetDiscountedPrice(2);
Assert.Equals(2, actual)
}
public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
var priceCalculator = new PriceCalculator();
var actual = priceCalculator.GetDiscountedPrice(2);
Assert.Equals(1, actual);
}
Malheureusement, vous réalisez rapidement qu’il existe des problèmes avec votre test :
- Si la suite de tests s'exécute le mardi, le deuxième test réussit, mais le premier échoue.
- Si la suite de tests s'exécute un autre jour, le premier test réussit, mais le second échoue.
Pour résoudre ces problèmes, vous devez introduire une couture dans votre code de production. Une approche consiste à encapsuler le code que vous devez contrôler dans une interface et que le code de production dépend de cette interface :
public interface IDateTimeProvider
{
DayOfWeek DayOfWeek();
}
public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
{
return price / 2;
}
else
{
return price;
}
}
Vous devez également écrire une nouvelle version de votre suite de tests :
public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
var priceCalculator = new PriceCalculator();
var dateTimeProviderStub = new Mock<IDateTimeProvider>();
dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);
var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
Assert.Equals(2, actual);
}
public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
var priceCalculator = new PriceCalculator();
var dateTimeProviderStub = new Mock<IDateTimeProvider>();
dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);
var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
Assert.Equals(1, actual);
}
La suite de tests a désormais un contrôle total sur la valeur DateTime.Now
et peut utiliser n'importe quelle valeur lors de l'appel de la méthode.