web-dev-qa-db-fra.com

Unité testant une classe avec une horloge Java 8

Java 8 a introduit Java.time.Clock, Qui peut servir d'argument à de nombreux autres objets Java.time, Vous permettant d'y injecter une horloge réelle ou fictive. Par exemple, je sais que vous pouvez créer une Clock.fixed(), puis appeler Instant.now(clock) et le résultat sera le Instant corrigé que vous avez fourni. Cela semble parfait pour les tests unitaires!

Cependant, j'ai du mal à trouver la meilleure façon de l'utiliser. J'ai une classe semblable à la suivante:

public class MyClass {
    private Clock clock = Clock.systemUTC();

    public void method1() {
        Instant now = Instant.now(clock);
        // Do something with 'now'
    }
}

Maintenant, je veux tester ce code à l’unité. Je dois pouvoir configurer clock pour produire des durées fixes de manière à pouvoir tester method() à des moments différents. Clairement, je pourrais utiliser la réflexion pour définir le membre clock sur des valeurs spécifiques, mais ce serait bien si je n'avais pas besoin de recourir à la réflexion. Je pourrais créer une méthode publique setClock(), mais je me sens mal. Je ne veux pas ajouter d'argument Clock à la méthode car le vrai code ne devrait pas être concerné par le passage d'une horloge.

Quelle est la meilleure approche pour gérer cela? C'est du nouveau code pour que je puisse réorganiser la classe.

Edit: Pour clarifier, je dois être capable de construire un seul objet MyClass mais de pouvoir lui faire voir deux valeurs d’horloge différentes (comme s’il s’agissait d’une horloge système régulière). En tant que tel, je ne peux pas transmettre une horloge fixe au constructeur.

57
Mike

Permettez-moi de mettre la réponse de Jon Skeet et les commentaires dans le code:

classe sous test:

public class Foo {
    private final Clock clock;
    public Foo(Clock clock) {
        this.clock = clock;
    }

    public void someMethod() {
        Instant now = clock.instant();   // this is changed to make test easier
        System.out.println(now);   // Do something with 'now'
    }
}

test unitaire:

public class FooTest() {

    private Foo foo;
    private Clock mock;

    @Before
    public void setUp() {
        mock = mock(Clock.class);
        foo = new Foo(mock);
    }

    @Test
    public void ensureDifferentValuesWhenMockIsCalled() {
        Instant first = Instant.now();                  // e.g. 12:00:00
        Instant second = first.plusSeconds(1);          // 12:00:01
        Instant thirdAndAfter = second.plusSeconds(1);  // 12:00:02

        when(mock.instant()).thenReturn(first, second, thirdAndAfter);

        foo.someMethod();   // string of first
        foo.someMethod();   // string of second
        foo.someMethod();   // string of thirdAndAfter 
        foo.someMethod();   // string of thirdAndAfter 
    }
}
40
Jeff Xiao

Je ne veux pas ajouter d'argument Clock à la méthode car le code réel ne devrait pas concerner le passage d'une horloge.

Non ... mais vous voudrez peut-être le considérer comme un paramètre constructeur. En gros, vous dites que votre classe a besoin d'une horloge avec laquelle travailler ... c'est donc une dépendance. Traitez-le comme toute autre dépendance et injectez-le dans un constructeur ou via une méthode. (Je préfère personnellement l'injection de constructeur, mais YMMV.)

Dès que vous cessez de penser à quelque chose que vous pouvez facilement construire vous-même et que vous commencez à le penser à "juste une autre dépendance", vous pouvez alors utiliser des techniques familières. (Je suppose que vous êtes à l'aise avec l'injection de dépendance en général, certes.)

42
Jon Skeet

Je suis un peu en retard pour le jeu ici, mais pour ajouter aux autres réponses suggérant l’utilisation d’une horloge, cela fonctionne sans aucun doute. En utilisant doock de Mockito, vous pouvez créer une horloge que vous pourrez ajuster de manière dynamique au fur et à mesure de vos tests.

Supposons cette classe, qui a été modifiée pour prendre une horloge dans le constructeur, et référence l'horloge aux appels Instant.now(clock).

public class TimePrinter() {
    private final Clock clock; // init in constructor

    // ...

    public void printTheTime() {
        System.out.println(Instant.now(clock));
    }
}

Ensuite, dans votre configuration de test:

private Instant currentTime;
private TimePrinter timePrinter;

public void setup() {
   currentTime = Instant.Epoch; // or Instant.now() or whatever

   // create a mock clock which returns currentTime
   final Clock clock = mock(Clock.class);
   when(clock.instant()).doAnswer((invocation) -> currentTime);

   timePrinter = new TimePrinter(clock);
}

Plus tard dans votre test:

@Test
public void myTest() {
    myObjectUnderTest.printTheTime(); // 1970-01-01T00:00:00Z

    // go forward in time a year
    currentTime = currentTime.plus(1, ChronoUnit.YEARS);

    myObjectUnderTest.printTheTime(); // 1971-01-01T00:00:00Z
}

Vous dites à Mockito de toujours exécuter une fonction qui renvoie la valeur actuelle de currentTime à chaque appel de instant (). Instant.now(clock) appellera clock.instant(). Maintenant, vous pouvez avancer rapidement, revenir en arrière et voyager dans le temps généralement mieux qu’un DeLorean.

25
Rik

Pour commencer, injectez certainement un Clock dans votre classe sous test, comme recommandé par @Jon Skeet. Si votre classe n'a besoin que d'une fois, passez simplement une valeur Clock.fixed(...). Toutefois, si votre classe se comporte différemment dans le temps, par exemple il fait quelque chose à l'heure A, puis fait autre chose à l'heure B, puis notez que les horloges créées par Java sont immuables , et ne peut donc pas être modifié par le test pour retourner l’heure A à un moment donné, puis l’heure B à un autre.

Se moquer, selon la réponse acceptée, est une option, mais lie étroitement le test à la mise en œuvre. Par exemple, comme l’a souligné un intervenant, que se passe-t-il si la classe sous test appelle LocalDateTime.now(clock) ou clock.millis() au lieu de clock.instant()?

Une autre approche un peu plus explicite, plus facile à comprendre et plus robuste qu’un simulacre est de créer une implémentation réelle de Clock qui soit mutable , afin que le test puisse l'injecter et le modifier si nécessaire. Ce n'est pas difficile à mettre en œuvre. Voir https://github.com/robfletcher/test-clock pour un bon exemple.

MutableClock c = new MutableClock(Instant.Epoch, ZoneId.systemDefault());
ClassUnderTest classUnderTest = new ClassUnderTest(c);

classUnderTest.doSomething()
assertTrue(...)

c.instant(Instant.Epoch.plusSeconds(60))

classUnderTest.doSomething()
assertTrue(...)
1
Raman