Apprendre à faire des tests unitaires en .Net Core avec XUNIT

Partie2 : AutoFixture

SORO Nacoumblé Olivier Martial

Dans la première partie du tutoriel, nous avons appris comment développer selon l’approche TDD à l’aide des tests unitaires et des Mocks. Dans cette seconde partie, nous allons explorer la bibliothèque AutoFixture qui offre des supports très intéressants pour faciliter l’écriture et la maintenance des tests unitaires.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum. Commentez

- Titre : Apprendre à faire des tests unitaires en .Net Core avec XUNIT

- Auteur : SORO Nacoumblé Olivier Martial

- Parution : 16 mars 2020

- Mise à jour : 16 mars 2020

- Licence : Copyright ® 2020 SORO Nacoumblé Olivier Martial. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- Apprendre à faire des tests unitaires en .Net Core avec XUNIT

Sommaire

- I. Le pattern AAA, structure d’écritures des TDD

- II. Introduction à AutoFixture

- III. Générer des données aléatoires avec AutoFixture

- III-A. Exemple 1 : Générer des données aléatoires pour un type basique

- III-B. Exemple 2 : Générer des données aléatoires pour un type spécifique

- IV. Personnaliser un objet avec AutoFixture

- V. Les tests paramétrés

- VI. Combiner AutoFixture et Moq

- VII. Conclusion

- VIII. Remerciements Developpez.com

I - Le pattern AAA, structure d’écritures des TDD

Avant de vous parler d’AutoFixture j’ai jugé utile de vous présenter le pattern AAA. Il s’agit d’un modèle devenu un standard dans l’écriture des tests unitaires. Il recommande de diviser la méthode de tests en trois parties : Arrange, Act et Assert.

Arrange

Comme la traduction le dit, c’est dans cette partie qu’on organise le test. On initialise les variables, on prépare un jeu de données en fonction de ce qu’on souhaite tester. C’est aussi ici qu’on définit le comportement attendu des objets mockés.

Act

C’est ici qu’on exécute le test en invoquant la méthode à tester avec les paramètres définis dans la partie Arrange.

Assert

C’est ici qu’on vérifie le résultat de notre test. On vérifie si le résultat obtenu est conforme au résultat attendu.

Avec cette structure le code de tests est bien clair et facile à comprendre par d’autres membres de l’équipe qui interviendront sur le projet. Tous les tests de la partie I de ce tutoriel ont été écrits selon ce pattern.

II - Introduction à AutoFixture

Allant du constat que bien souvent le développeur dépense beaucoup de temps à initialiser les variables, à préparer un jeu de données ou encore à maintenir la partie Arrange dans une méthode de tests unitaires, la bibliothèque Autofixture nous vient en aide, car elle offre des fonctionnalités intéressantes afin d’alléger le travail à fournir dans cette partie de la méthode de tests. Dans ce tutoriel nous allons explorer certaines fonctionnalités qu’elle nous offre.

Tout d’abord nous allons commencer par installer AutoFixture dans notre projet de tests Xunit.

Install-Package AutoFixture -Version 4.11.0

III - Générer des données aléatoires avec AutoFixture

À l’aide de la méthode Create, il est possible de générer des données aléatoires de n’importe quel type d’objet. Elle est utilisée dans le cas où l’on souhaite écrire un test dont le contenu des variables nous importe peu.

III-A - Exemple 1 : Générer des données aléatoires pour un type basique

Autofixture peut être utilisé pour générer des données aléatoires pour les objets de types basiques (string, char, int, DateTime, float, decimal, etc.).

Nous allons expliquer ce concept à l’aide de cet exemple.

On souhaite écrire un test qui détermine la prime des employés :

- chaque employé a un score allant de 0 à 10 ;

- le montant de la prime est égal au score multiplié par 1000 c’est-à-dire l’employé qui aura reçu une note de 8,5/10 aura un bonus de 8500 $ ;

- par contre le montant minimum de la prime est de 6000 $. Tous les employés qui recevront un score inférieur à 6 recevront 6000 $ comme prime.

Écrivons une fonction qui vérifie que la prime minimum est de 6000 $, quel que soit le score de l’employé.

La fonction à tester est :

public class BonusCalculator
{
public float GetPrime(float score)
{
return 0;
}
}

Dans la partie arrange de notre méthode de tests nous allons demander à AutoFixture de nous générer une valeur de type Float de manière aléatoire qui représentera le score de l’employé.

//Arrange
var fixture = new Fixture();
var score = fixture.Create<float>();

La méthode de tests sera :

[Fact]
public void GivenScore_WhenGettingPrime_ThenReturnValueGreaterThanOrEqualTo6000()
{
//Arrange
var fixture = new Fixture();
var score = fixture.Create<float>();
var bonusCalculator = new BonusCalculator();

//Act
var actualValue = bonusCalculator.GetPrime(score);

//Assert
Assert.True(actualValue >= 6000);
}

Le code de notre fonction à tester sera alors :

public double GetPrime(float score)
{
return score > 6 ? score * 1000 : 6000;
}

Je rappelle que ce test n’a pas pour vocation de vérifier que le score est réellement compris entre 0 et 10. Une autre méthode de tests peut être écrite pour cela, la fonction à tester sera donc adaptée en conséquence.

Toutefois on peut encore préciser à AutoFixture de nous choisir une valeur entre 0 et 10 pour la valeur du score grâce à la propriété Customizations de la classe Fixture.

var fixture = new Fixture();
fixture.Customizations.Add(new RandomNumericSequenceGenerator(0, 10));

III-B - Exemple 2 : Générer des données aléatoires pour un type spécifique

AutoFixture peut être utilisé pour générer des données aléatoires pour une classe comportant des propriétés.

Dans cet exemple on veut écrire une fonction qui calcule le bonus d’un employé en fonction de son grade et de son score :

- si l’employé possède le grade A et a plus de cinq années d’ancienneté, le bonus est égal au score multiplié par 2000 $. S’il a moins de cinq années d’ancienneté, le bonus est égal au score multiplié par 1000 $ ;

- sinon, le bonus est égal au score multiplié par 500 $ ;

- dans tous les cas le bonus minimum est de 2500 $. Si la multiplication du score par le coefficient de la prime est inférieure à cette valeur, l’employé recevra 2500 $ comme prime.

On veut écrire la méthode de tests qui vérifie que le bonus retourné par notre fonction est toujours supérieur ou égal à 2500 $.

public class Employee
{
public string EmployeeId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public char Grade { get; set; }
public int ServiceYears { get; set; }

public Employee(string employeeId, string firstName, string lastName, char grade)
{
EmployeeId = employeeId;
FirstName = firstName;
LastName = lastName;
Grade = grade;
}
}

public double GetPrime(Employee employee, float score)
{
return 0;
}

Ici on ne peut pas déclarer un nouvel employé sans préciser son nom, son prénom, son matricule et son grade. Ce qui implique que si on rajoute une autre propriété dans notre constructeur, on devra aussi modifier toutes les méthodes de tests qui instancient la classe Employee. Ça sera un travail de maintenance supplémentaire à fournir par les développeurs.

Écrivons notre méthode de tests sans utiliser AutoFixture :

[Fact]
public void GivenEmployeeAndScore_WhenGettingPrime_ThenReturnValueGreaterThanOrEqualTo2500()
{
//Arrange
var employee = new Employee("IdTest","FirstNameTest","LastNameTest",'A');
employee.ServiceYears = 4;
var bonusCalculator = new BonusCalculator();
var employeeScore = 8;

//Act
var actualValue = bonusCalculator.GetPrime(employee, employeeScore);

//Assert
Assert.True(actualValue >= 2500);
}

Comme mentionné plus haut, nous avons été contraints d’assigner des valeurs aux propriétés EmployeeId, FirstName, LastName, Grade, ServiceYears ensuite au paramètre score avant de déclencher l’appel de notre fonction à tester, bien que les trois premières citées n’aient pas de rôle à jouer dans ce test en question.

Aussi on peut remarquer que cette méthode de tests ne couvre pas tous les cas. On ne peut pas tester que la condition est vérifiée quand l’employé a plus de cinq années d’expérience ni lorsque ce dernier possède le grade B. Ce qui veut dire qu’il nous faut rajouter d’autres méthodes de tests, juste pour vérifier que le bonus est toujours supérieur ou égal à 2500 $.

Avec AutoFixture les choses deviennent beaucoup plus simples, car on peut se contenter de vérifier notre assertion, quelles que soient les valeurs de nos paramètres concernés par le calcul de la prime.

[Fact]
public void GivenEmployeeAndScore_ ThenReturnValueGreaterThanOrEqualTo2500_ThenReturnValueGreaterThan2500()
{
//Arrange
var fixture = new Fixture();
var employee = fixture.Create<Employee>();
var employeeScore = fixture.Create<float>();
var bonusCalculator = new BonusCalculator();

//Act
var actualValue = bonusCalculator.GetPrime(employee, employeeScore);

//Assert
Assert.True(actualValue >= 2500);
}

À la création de l’objet « employee », AutoFixture attribuera des valeurs aléatoires aux propriétés Grade et ServiceYears. Il fera pareil à la création de l’objet « employeeScore ». Si le test fonctionne alors nous sommes certains que c’est peu importe les valeurs que peuvent prendre ces différents paramètres cités.

Pour faire fonctionner le test :

public float GetPrime(Employee employee, float score)
{
float prime;
if (employee.Grade == 'A')
{
prime = employee.ServiceYears > 5 ? 2000 * score : 1000 * score;
}
else
{
prime = 500 * score;
}
return prime > 2500 ? prime : 2500;
}

Compléments

1. AutoFxiture génère des données uniquement pour les propriétés publiques.

2. Pour générer une collection de données, on utilise la méthode CreateMany :

IEnumerable<float> scores = fixture.CreateMany<float>();
var employees = fixture.CreateMany<Employee>(100);
// Création d'une liste de 100 employés

IV - Personnaliser un objet avec AutoFixture

Grâce à la méthode Build on peut personnaliser la valeur des propriétés des objets de nos méthodes de tests.

Une fois de plus on va s’appuyer sur un exemple pour expliquer ce concept.

On veut rajouter une condition à l’exemple précédent :

- si l’employé possède 0 année d’ancienneté, il reçoit un bonus de 2500 $, quels que soient son grade et son score.

On peut utiliser la méthode Build pour fixer la valeur du nombre d’années d’expérience à 0 afin de tester cette condition.

[Fact]
public void GivenEmployeeWith0ServiceYear_WhenGettingPrime_ThenReturn2500()
{
//Arrange
var fixture = new Fixture();
var employee = fixture.Build<Employee>().With(x => x.ServiceYears, 0).Create();
var employeeScore = fixture.Create<float>();
var bonusCalculator = new BonusCalculator();

//Act
var actualValue = bonusCalculator.GetPrime(employee, employeeScore);

//Assert
Assert.Equal(2500, actualValue);
}

Pour que le test fonctionne, il faut donc modifier la méthode GetPrime :

public float GetPrime(Employee employee, float score)
{
float prime = 0;
if (employee.ServiceYears > 0)
{
if (employee.Grade == 'A')
{
prime = employee.ServiceYears > 5 ? 2000 * score : 1000 * score;
}
else
{
prime = 500 * score;
}
}
return prime > 2500 ? prime : 2500;
}

V - Les tests paramétrés

Il existe deux attributs pour définir une méthode de tests unitaires dans XUnit :

- l’attribut [Fact], utilisé dans tous les tests unitaires que nous avons vus jusqu’à présent. Ce sont des tests qui retournent toujours Vrai et sans condition particulière ;

- l’attribut [Theory] utilisé pour les tests qui retournent Vrai uniquement pour un jeu de données particulier. Alors pour ces tests-là, il est possible de passer le jeu de données en paramètre à la méthode de tests.

Exemple de tests avec l’attribut [Theory]

On reste toujours dans le même contexte de calcul de prime pour les employés. Cette fois, on nous dit que la prime est fixe en fonction du grade des employés :

- les employés de grade A recevront 3000 $ ;

- ceux du grade B, 2000 $ ;

- les employés de grade C recevront à leur tour 1000 $ ;

- les autres ne recevront aucune prime.

On veut écrire une méthode qui teste que le bon montant de prime est retourné en fonction du grade passé en paramètre.

On pourrait écrire notre méthode de tests en procédant ainsi :

[Fact]
public void GivenGrade_WhenGettingPrime_ThenReturnCorrectPrimeValue()
{
//Arrange
var bonusCalculator = new BonusCalculator();

//Act
var valueA = bonusCalculator.GetPrimeByGrade('A');
var valueB = bonusCalculator.GetPrimeByGrade('B');
var valueC = bonusCalculator.GetPrimeByGrade('C');
var otherValue = bonusCalculator.GetPrimeByGrade('D');

//Assert
Assert.Equal(3000, valueA);
Assert.Equal(2000, valueB);
Assert.Equal(1000, valueC);
Assert.Equal(0, otherValue);
}

Ce test fonctionne correctement certes, mais dans ce genre de cas, il est plus simple de passer les jeux donnés en paramètres à la méthode de tests à l’aide de l’attribut InlineData.

Pour pouvoir utiliser InlineData, il faut rajouter le package ci-dessous à notre méthode de tests :

Install-Package AutoFixture.Xunit2 -Version 4.11.0

Maintenant, réécrivons notre méthode de tests avec InlineData :

[Theory]
[InlineData('A', 3000)]
[InlineData('B', 2000)]
[InlineData('C', 1000)]
[InlineData('D', 0)]
public void GivenGrade_WhenGettingPrime_ThenReturnCorrectPrimeValue(char grade, float prime)
{
//Arrange
var bonusCalculator = new BonusCalculator();

//Act
var actualValue = bonusCalculator.GetPrimeByGrade(grade);

//Assert
Assert.Equal(prime, actualValue);
}

Pour chaque cas à tester, le grade et le résultat attendu sont passés en paramètres à notre méthode de tests. La fonction à tester est appelée une seule fois, mais sera exécutée quatre fois avec nos différents jeux de données définis dans InlineData. Pareil pour la partie Assert de notre test, elle sera aussi exécutée à quatre reprises.

VI - Combiner AutoFixture et Moq

Il est tout à fait possible de combiner AutoFixture et la bibliothèque Moq pour mocker des dépendances à notre classe à tester. Pour explorer cela, nous allons reprendre les exemples de la partie I du tutoriel.

Cette fois-ci la classe ArrayManagement possède deux méthodes GetSumOfItem.

- L’une des méthodes récupère un tableau d’entiers depuis la base de données et retourne la somme des entiers de ce tableau.

- L’autre méthode récupère un tableau d’entiers depuis un fichier et retourne la somme des entiers de ce tableau.

public class ArrayManagement
{
IDatabaseManagement _databaseManagement;
IFileSystem _fileSystem;
public ArrayManagement(IDatabaseManagement databaseManagement, IFileSystem fileSystem)
{
_databaseManagement = databaseManagement;
_fileSystem = fileSystem;
}

public int GetSumOfItemsFromDataBase()
{
…
}

public int GetSumOfItemsFromFile(string path)
{
…
}

}

Contrairement à l’exemple du tutoriel I, le constructeur de notre classe ArrayManagement contient deux interfaces passées en paramètres. On veut écrire la méthode de tests qui vérifie si la classe GetSumOfItemsByDataBase retourne la bonne valeur.

Dans ce cas précis, on n’aimerait pas avoir affaire à IfileSystem, car cette dernière n’intervient pas dans la fonctionnalité à tester.

La classe AutoMoqCustommization d’AutoFixture nous permet de mocker par défaut toutes les dépendances qui sont utilisées dans le constructeur de notre classe à tester.

Pour utiliser AutoMoqCustommization, il faut installer le package :

Install-Package AutoFixture.AutoMoq -Version 4.11.0

Écrivons à présent notre classe de test :

[Theory]
[InlineData(new int[5] { 3, 4, 5, 7, 10 }, 29)]
public void GivenArrayOfIntegers_WhenGettingSumOfItemsFromDatabase_ThenReturnSumOfEachItems(int[] array, int expectedValue)
{
//Arrange
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var arrayManagement = fixture.Create<ArrayManagement>();

//Act
var actualValue = arrayManagement.GetSumOfItemsFromDataBase();

//Assert
Assert.Equal(expectedValue, actualValue);
}

Dans la partie Arrange de notre test, on peut implémenter un Mock de l’interface IDatabaseManagement puisque c’est cette dernière qui est censée nous retourner le tableau d’entiers. Vous remarquerez qu’une fois de plus on ne se soucie guère du constructeur de la classe ArrayManagement lors de l’instanciation de notre objet.

La méthode de tests sera :

[Theory]
[InlineData(new int[5] { 3, 4, 5, 7, 10 }, 29)]
public void GivenArrayOfIntegers_WhenGettingSumOfItemsByDatabase_ThenReturnSumOfEachItems(int[] array, int expectedValue)
{
//Arrange
var fixture = new Fixture().Customize(new AutoMoqCustomization());
var databaseManagement = fixture.Freeze<Mock<IDatabaseManagement>>();
databaseManagement.Setup(x => x.GetItemsFromDatabase()).Returns(array);
var arrayManagement = fixture.Create<ArrayManagement>();

//Act
var actualValue = arrayManagement.GetSumOfItemsFromDataBase();

//Assert
Assert.Equal(expectedValue, actualValue);
}

On remarque qu’aucune modification du constructeur de la classe ArrayManagement, ni de l’interface IFileSystem ne nous poussera à modifier cette méthode de tests.

La fonction testée :

public int GetSumOfItemsFromDataBase()
{
var items = _databaseManagement.GetItemsFromDatabase();
if (items == null)
{
throw new ArgumentNullException();
}
var value = 0;
foreach (var item in items)
{
value += item;
}
return value;
}

VII - Conclusion

Nous sommes arrivés au terme de ce tutoriel, j’espère que cette petite présentation de l’outil AutoFixture a été utile pour vous et vous permettra d’améliorer l’écriture, la qualité et la maintenance de vos tests unitaires.

VIII - Remerciements Developpez.com

Nous tenons à remercier Malick pour la mise au gabarit et ClaudeLELOUP pour la relecture orthographique.