Stubs Objects Java

a marqué ce sujet comme résolu.

Salut,

Les stubs (et plus généralement, tout ce qui touche aux mocks) ne servent pas à tester les classes, mais plutôt à contrôler du code que tu ne souhaites pas tester dans le contexte de ton test. C’est particulièrement utile lorsque ton code dépend d’un service qui ne t’appartient pas, car tu ne souhaites pas tester le fonctionnement de ce service, mais uniquement ton code à toi.

Par exemple, imagine que tu écris un programme dont le but est par exemple de lire les flux RSS de Zeste de Savoir. Ce que tu veux tester, c’est que ton code est bien capable de lire le contenu XML du flux, mais en aucun cas tu ne veux tester le XML en lui-même. De plus, tu veux vérifier que ton code réagit correctement aux cas où Zeste de Savoir tomberait en panne ou à un contenu XML invalide, mais tu ne veux pas attendre que cela arrive pour pouvoir tester ce genre de chose. Pour résumer, tu n’as pas le contrôle de Zeste de Savoir.

Le stub va donc te permettre de dire à ton test : « Ici, mon code à tester va faire appel à ce service externe que je ne contrôle pas, mais quand tu vas l’appeler, uniquement dans le contexte d’un test, tu ne vas pas l’appeler mais faire à la place cette autre chose sur laquelle j’ai totalement la main ».

C’est aussi très pratique quand ton code dépend fortement de ce que te retourne une librairie.

PS : les mocks, stubs et espions ne sont en réalité pas limités à Java, on retrouve ces concepts dans tous les langages ;)

+5 -0

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.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte