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
- AutoFxiture génère des données uniquement pour les propriétés publiques.
- 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.