Test unitaire d'une classe avec une horloge Java 8


Java 8 a introduit java.time.Clock qui peut être utilisé comme argument pour de nombreux autres objets java.time, vous permettant d'y injecter une horloge réelle ou fausse. Par exemple, je sais que vous pouvez créer un Clock.fixed(), puis appeler Instant.now(clock) et il renverra le Instant fixe 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, similaire à 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é. J'ai besoin d' pour pouvoir définir clock pour produire des temps fixes afin que je puisse tester method() à différents moments. De toute évidence, 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 à recourir à la réflexion. Je pourrais créer une méthode publique setClock(), mais cela semble faux. Je ne veux pas ajouter un argument Clock à la méthode car le code réel ne devrait pas être concerné par le passage d'une horloge.

Quelle est la meilleure approche pour gérer cela? C'est un nouveau code donc je pourrais réorganisez la classe.

Edit: Pour clarifier, je dois pouvoir construire un seul MyClass objet mais pouvoir faire voir à cet objet 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 passer une horloge fixe dans le constructeur.

Author: Mike, 2014-11-21

6 answers

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

Classe testée:

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 
    }
}
 54
Author: XoXo, 2014-11-21 18:42:24

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

Non... mais vous voudrez peut-être le considérer comme un paramètre constructeur. Fondamentalement, vous dites que votre classe a besoin d'une horloge avec laquelle travailler... donc, c'est une dépendance. Traitez-le comme vous le feriez pour toute autre dépendance et injectez-le dans un constructeur ou via une méthode. (Personnellement, je privilégie l'injection du constructeur, mais YMMV.)

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

 58
Author: Jon Skeet, 2014-11-21 17:34:08

Je suis un peu en retard dans le jeu ici, mais pour ajouter aux autres réponses suggérant d'utiliser une horloge - cela fonctionne certainement, et en utilisant doAnswer de Mockito, vous pouvez créer une horloge que vous pouvez ajuster dynamiquement à mesure que vos tests progressent.

Supposons cette classe, qui a été modifiée pour prendre une horloge dans le constructeur, et référencez l'horloge sur les 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
}

Tu dis à Mockito de toujours exécutez une fonction qui renvoie la valeur actuelle de currentTime chaque fois que instant() est appelé. Instant.now(clock) appellera clock.instant(). Maintenant, vous pouvez avancer rapidement, rembobiner et voyager dans le temps mieux qu'une DeLorean.

 28
Author: Rik, 2017-04-29 07:40:41

Créer une horloge Mutable au lieu de se moquer

Pour commencer, injectez définitivement un Clock dans votre classe en test, comme recommandé par @Jon Skeet. Si votre classe ne nécessite qu'une seule fois, passez simplement une valeur Clock.fixed(...). Cependant, si votre classe se comporte différemment dans le temps, par exemple, elle fait quelque chose au moment A, puis fait autre chose au moment B, notez que les horloges créées par Java sont immuables, et ne peuvent donc pas être modifiées par le test pour renvoyer l'heure A à la fois, et puis le temps B à un autre.

Se moquer, selon la réponse acceptée, est une option, mais associe étroitement le test à l'implémentation. Par exemple, comme le souligne un commentateur, que se passe-t-il si la classe testée appelle LocalDateTime.now(clock) ou clock.millis() au lieu de clock.instant()?

Une approche alternative un peu plus explicite, plus facile à comprendre et peut être plus robuste qu'une simulation, consiste à créer une véritable implémentation de Clock qui est mutable , afin que le test puisse l'injecter et le modifier comme nécessaire. Ce n'est pas difficile à mettre en œuvre, ou voici deux implémentations prêtes à l'emploi:

Et voici comment on pourrait utiliser quelque chose comme ceci dans un test:

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(...)
 7
Author: Raman, 2021-01-21 22:49:42

Comme d'autres l'ont noté, vous devez vous en moquer d'une manière ou d'une autre-mais il est assez facile de lancer le vôtre:

    class UnitTestClock extends Clock {
        Instant instant;
        public void setInstant(Instant instant) {
            this.instant = instant;
        }

        @Override
        public Instant instant() {
            return instant;
        }

        @Override
        public ZoneId getZone() {
            return ZoneOffset.UTC;
        }

        @Override
        public Clock withZone(ZoneId zoneId) {
            throw new UnsupportedOperationException();
        }
    }
 1
Author: Matthew, 2020-05-28 13:19:29

J'ai fait face au même problème et je ne pouvais pas la solution existante qui est simple et qui fonctionne, donc j'ai fini par suivre le code. Bien sûr, cela peut être mieux, a TZ configurable, etc., mais j'avais besoin de quelque chose de facile à utiliser dans les tests, où je veux vérifier comment ma classe sous-test traite l'horloge:

  1. Je ne veux pas me soucier de quelle méthode particulière du Clock est appelée, donc se moquer n'est pas un chemin à parcourir.
  2. Je voulais que les millies soient définis dès le départ

Remarque: cette classe est Groovy class, à utiliser dans les tests Spock, mais il est facilement traduisible en Java.

class FixedTicksClock extends Clock {
    private ZoneId systemDefault = ZoneId.systemDefault()
    private long[] ticks
    private int current = 0

    FixedTicksClock(long ... ticks) {
        this.ticks = ticks
    }

    @Override
    ZoneId getZone() {
        systemDefault
    }

    @Override
    Clock withZone(final ZoneId zone) {
        systemDefault = zone
        this
    }

    @Override
    Instant instant() {
        ofEpochMilli(getNextTick())
    }

    @Override
    long millis() {
        getNextTick()
    }

    private long getNextTick() {
        if (current >= ticks.length) {
            throw new IllegalStateException('No more ticks provided')
        } else {
            ticks[current++]
        }
    }
}
 0
Author: Krzysztof Wolny, 2020-10-05 15:35:50