Lorsqu’on écrit nos tests unitaires, on peut avoir l’impression que certains cas de tests sont quasiment identiques dans leur structure. Ils sont rébarbatifs à lire/écrire. C’est là que la notion de dataProvider est utile: on va réutiliser le même test mais avec des paramètres d’entrée différents 😉
On a le premier joueur qui marque x fois, le second joueur qui marque x fois et enfin on compare avec le résultat attendu.
Les tests peuvent vite se multiplier et se ressembler:
@Test
void shouldBe15LoveWhenPlayerOneScore() {
Game game = new Game();
game.playerOneScoresAPoint();
String score = game.printScore();
assertThat(score).isEqualTo("15 - LOVE");
}
@Test
void shouldBe15-15WhenBothPlayersScore() {
Game game = new Game();
game.playerOneScoresAPoint();
game.playerTwoScoresAPoint();
String score = game.printScore();
assertThat(score).isEqualTo("15 - 15");
}
@Test
void shouldBe30LoveWhenPlayerOneScore() {
Game game = new Game();
game.playerOneScoresAPoint();
game.playerOneScoresAPoint();
String score = game.printScore();
assertThat(score).isEqualTo("30 - LOVE");
}
C’est assez rébarbatif et on se retrouve rapidement avec une classe de test qui fait plusieurs dizaines / centaines de lignes. On va voir dans cet article comment rendre nos tests plus lisibles et maintenables grâce aux tests paramétrés 😀
Cette bibliothèque va nous permettre de passer des paramètres d’entrées à nos tests. Ces paramètres proviendront d’une source de données (une méthode, un CSV, un tableau etc..).
Pour marquer un test comme un test paramétré, on va utiliser l’annotation @ParametrizedTest.
Exemple:
@ParameterizedTest
void shouldGetGameScore(int timesPlayerOneScores, int timesPlayerTwoScores, String expectedGameScore) {
Ensuite, on doit décrire d’où proviennent les données d’entrées (notre data-provider) avec une autre annotation.
Avec @MethodSource j’indique que mes données viennent de la méthode nommée “gameProvider”.
Exemple:
@ParameterizedTest
@MethodSource("gameProvider")
void shouldGetGameScore(int timesPlayerOneScores, int timesPlayerTwoScores, String expectedGameScore) {
Un test unitaire sera exécuté par chaque argument. Par exemple, pour le premier argument Arguments.of(0, 0, « LOVE – LOVE »), mon test unitaire sera lancé avec:
– Le premier paramètre (timesPlayerOneScores) qui vaut 0
– Le second paramètre (timesPlayerTwoScores) qui vaut 0 également
– Le dernier paramètre (expectedGameScore) qui a pour valeur “LOVE – LOVE”
Note: D’autres façons de transmettre de la donnée existent. On peut lire un CSV, une énum ou même donner un tableau d’entrée.
Exemple concret du cas d’usage décrit en introduction
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
class TennisTest {
private Game game;
@ParameterizedTest
@MethodSource("gameProvider")
void shouldGetGameScore(int timesPlayerOneScores, int timesPlayerTwoScores, String expectedGameScore) {
game = new Game();
playerOneScores(timesPlayerOneScores);
playerTwoScores(timesPlayerTwoScores);
String score = game.printScore();
assertThat(score).isEqualTo(expectedGameScore);
}
private void playerOneScores(int timesPlayerOneScores) {
for (int i = 0; i < timesPlayerOneScores; i++) {
game.playerOneScoresAPoint();
}
}
private void playerTwoScores(int timesPlayerTwoScores) {
for (int i = 0; i < timesPlayerTwoScores; i++) {
game.playerTwoScoresAPoint();
}
}
public static Stream<Arguments> gameProvider() {
return Stream.of(
Arguments.of(0, 0, "LOVE - LOVE"),
Arguments.of(1, 0, "15 - LOVE"),
Arguments.of(2, 0, "30 - LOVE"),
Arguments.of(3, 0, "40 - LOVE"),
Arguments.of(0, 1, "LOVE - 15"),
Arguments.of(2, 3, "30 - 40"),
Arguments.of(3, 3, "DEUCE")
);
}
}
En lançant mes tests, on a bien le détail de l’exécution:
Conclusion
Si vous avez la sensation d’écrire plusieurs fois des tests similaires, un test paramétrés pourra peut-être vous permettre de gagner en temps d’écriture et en lisibilité. C’est un outil que j’utilise au quotidien et j’espère que je vous aurais convaincu de son intérêt 😀
Il arrive que la code review nous empêche de merge nos PR assez vite et qu’on se retrouve à tirer une branche d’une branche de travail pour avancer 😔
Une fois la première PR squash et merge, la PR issue de la seconde branche se retrouve avec des conflits 💥
Si vous avez déjà vécu cette situation, il y a de bonnes chances que vous ayez cherry-pick vos commits de travail sur une...
Il arrive souvent que pour tester unitairement des règles de validation, on doive tester le même cas mais avec des exemples différents. Sans tests paramétrés, ça revient à faire un test par cas ce qui peut alourdir notre fichier de tests.
On va voir comme...
Avant .Net 8, tester du code qui utilise DateTime.Now() n’était pas trivial, on devait faire en sorte de mocker la Clock dans nos tests. Depuis .Net 8, c’est beaucoup plus facile grâce à TimeProvider inclut par défaut !
En deux mots, TimeProvider est une...
La gestion des erreurs de son API est très importante pour que les consommateurs puissent avoir une description claire du problème mais c’est souvent fastidieux à maintenir.
On va voir comment avoir des statuts de réponse cohérent et des messages d’erre...