Un stub va remplacer un composant dont il proposera le même comportement (i.e. même interface) mais il fournira toujours les mêmes résultats quels que soient ses paramètres (une fonction retournera toujours la même valeur).
Ils sont très utile dans les tests parce que :
- les tests doivent être F.I.R.S.T. Donc un stub va remplacer un composant qui a besoin d’accéder au système de fichiers, réseau, etc. manipuler l’heure, de l’aléatoire, effectuer des opérations longues, etc.
- les tests unitaires se concentrent sur une unité de code (une méthode, un objet, etc.) et qu’on a besoin de maîtriser son environnement (i.e. les composants dont dépend le code qu’on teste unitairement)
- on n’a pas besoin d’appeler ou on ne doit pas appeler le composant stubbé (on parle aussi de dummy)
- avec l’inversion de dépendances (et SOLID en général), c’est facile de stubber
Les stubs sont aussi très utiles dans le code en prod pour simuler le comportement d’un composant qui sera développé plus tard/par une autre équipe. Très utile notamment quand tu te concentres sur le métier et que tu veux repousser les choix techniques au plus tard (archi hexagonal, par exemple)
Les stub font partie de ce qu’on appelle les Test Doubles :
On parle de test doubles en référence aux doublures cinéma qui prennent la place des vrais acteurs pendant certaines scènes. Là, c’est pareil : on remplace des objets par d’autres dans un contexte particulier (tests, démos, MVP, etc.)
les dummys
Il y deux types de dummys :
- les dummy values : des valeurs dont la valeur n’a aucune importance
- les dummy objects : des objets qui ne sont pas utilisés. Si c’est le cas, on remontera une erreur car le test ne fera pas ce qu’on attend
Des objets dont la valeur n’apporte rien au test mais qui sont tout de même obligatoire pour construire ton test
On les utilise en passage de paramètre de la fonction que tu veux tester.
Ca permet de simplifier les tests et de les rendre plus lisibles. Ca permet aussi de bien expliciter ce qui n’est pas utilisé dans le test
Un exmple de dummy value :
@Test
public void should_return_HIGH_when_is_mig() {
final UUID dummyUUID = UUID.nameUUIDFromBytes("Dummy UUID".getBytes());
final Position dummyPosition = new Position(48.7667, -3.05, 50.0);
final Velocity dummyVelocity = new Velocity(100.0, 0.0, 0.0);
final Area dummyArea = new ComposedArea();
final Aircraft mig = new Aircraft(dummyUUID, dummyPosition, dummyVelocity, AircraftType.Mig);
Optional<ThreatLevel> result = isMig.evaluate(mig, dummyArea);
Assertions.assertEquals(Optional.of(ThreatLevel.High), result);
}
Dans ce test, il n’y a que le type d’avion (Mig) qui importe. Mais un Aircraft
nécessite plusieurs paramètres. On lui passera donc des dummys
Un exemple de dummy objet :
class DummyGeographicalRepository implements GeographicalRepository {
@Override
public Area getAreaNear(Position ownLocation) {
throw new ShouldNotBeCalledException();
}
}
@Test
public void should_add_rules_in_order() {
final Position dummyPosition = new Position(48.7667, -3.05, 50.0);
final Aircraft dummyAircraft = new Aircraft(UUID.nameUUIDFromBytes("Dummy UUID".getBytes()),
dummyPosition, new Velocity(100.0, 0.0, 0.0), AircraftType.F35);
Classifier classifier = new Classifier(new DummyGeographicalRepository(), dummyPosition)
.with((aircraft, areasInSector) -> Optional.of(ThreatLevel.Low))
.with((aircraft, areasInSector) -> Optional.of(ThreatLevel.High));
Assertions.assertEquals(2, classifier.rules().size());
}
On a besoin d' un repository pour construire un Classifier
. Mais on sait que, pour ce test, il ne sera pas utilisé parce que ce n’est le but du test.
Les stubs
Les stubs sont des objets qui retournent des valeurs en dur. Ils vont implémenter une version très spécifique des interfaces pour remplacer l’implémentation réelle.
On les utilisera soit parce que le composant réel n’existe pas, soit parce qu’il dépend d’éléments qu’on ne maîtrise pas (une connexion à une base, par exemple)
Les stubs sont utiles pour atteindre un cas particulier ou mettre le système dans un état précis
public class GeographicalRepositoryStub implements GeographicalRepository {
@Override
public Area getAreaNear(Position ownLocation) {
return new PlainArea(new Position(50.0, 50.0, 50.0), 50);
}
}
@Test
public void should_add_rules() {
Classifier classifier = new Classifier(new DummyGeographicalRepository(), dummyPosition)
.with((aircraft, areasInSector) -> Optional.of(ThreatLevel.Low))
.with((aircraft, areasInSector) -> Optional.of(ThreatLevel.High));
Assertions.assertEquals(2, classifier.rules().size());
}
Dans ce test le stub nous permet d’ancrer la position du système à la valeur qu’on souhaite
Spy
Les spy sont des objets qui vont capturer les entrées/sorties, les manipuler ou non avant d’interagir avec le système qu’on teste
On pourra ainsi observer des comportements et effets de bord qu’on ne voit pas normalement.
Ils sont très utiles pour les tests techniques (vérifier l’appel d’une méthode, déclencher des cas d’erreur, etc.)
class SpyRule implements EvaluationRule {
public int count = 0;
@Override
public Optional<ThreatLevel> evaluate(Aircraft aircraft, Area CloseArea) {
++count;
return Optional.empty();
}
}
@Test
public void should_apply_all_rules_until_match() {
SpyRule spy = new SpyRule();
Classifier classifier = new Classifier(new GeographicalRepositoryStub(), dummyPosition)
.with(spy).with(spy)
.with((aircraft, areasInSector) -> Optional.of(ThreatLevel.High)).with(spy);
classifier.getThreatLevelFor(dummyAircraft);
Assertions.assertEquals(2, spy.count);
}
Ce spy permet de vérifier combien de fois la méthode evaluate
est appelée
Mocks
C’est un stub avec de l’intelligence. On va pouvoir configurer les valeurs retournées (et autres entrées/sorties indirectes) en fonction des paramètres d’entrées.
Je n’utilise pas de mocks dans mes tests : on a vite tendance à tomber dans le piège des tests boîtes blanches ; ça casse un peu la dynamique des tests (given, when, then) et ces derniers sont souvent difficiles à écrire…
Mockito est sans doute le framework le plus connu pour faire des mocks (pour le coup, faire des mocks à la main est compliqué)
Fakes
Les fakes sont une version simplifiée des vrais objets. Ils sont là pour simuler le vrai comportement mais sans la machinerie dont on a besoin (connexion réseau, fichiers, base de données, etc.)
class GeographicalRepositoryFake implements GeographicalRepository {
private List<Area> areas = new ArrayList<>();
void add(Area area) {
areas.add(area);
}
@Override
public Area getAreaNear(Position ownLocation) {
return null;
}
}
@Test
public void Should_apply_rules_in_order() {
final Position dummyPosition = new Position(48.7667, -3.05, 50.0);
final Aircraft dummyAircraft = new Aircraft(UUID.nameUUIDFromBytes("Dummy UUID".getBytes()),
dummyPosition, new Velocity(100.0, 0.0, 0.0), AircraftType.F35);
GeographicalRepositoryFake repository = new GeographicalRepositoryFake();
repository.add(new PlainArea(new Position(30.0, 30.0, 5000.0), 100));
Classifier classifier = new Classifier(repository, dummyPosition)
.with((aircraft, areasInSector) -> Optional.of(ThreatLevel.Low))
.with((aircraft, areasInSector) -> Optional.of(ThreatLevel.High));
Assertions.assertEquals(ThreatLevel.Low, classifier.getThreatLevelFor(dummyAircraft));
}
Ce fake remplace la base données des zones Area
par une simple liste.
C’est une version condensée de ce que sont les test doubles. Personne ne te demandera de faire la différence entre un mock et un stub parce que ce n’est pas important.
La seule chose qu’il faut retenir, c’est que tu dois maîtriser ton environnement de tests de bout en bout. Et que ton code doit être conçu pour être testable facilement et rapidement.