Suppose I have this class, ClassToBeTested
, where the time elapsed between its method calls are important. foo
returns a different value depending on whether you have called foo
in the last x hours.
The way I implemented this is to get the current Instant
when foo
is called, and compare it with the last Instant
that foo
is called, which I store in a field.
Since ClassToBeTested
depends on the current time, I added a Clock
field, which callers are required to pass in when they create a ClassToBeTested
:
class ClassToBeTested {
private final Clock clock;
private Instant lastCalled;
public ClassToBeTested(Clock clock) {
this.clock = clock;
}
public String foo() {
if (lastCalled == null || Duration.between(lastCalled, Instant.now(clock)).compareTo(Duration.ofHours(1)) >= 0) {
lastCalled = Instant.now(clock);
return "A";
} else {
lastCalled = Instant.now(clock);
return "B";
}
}
}
When I am writing my tests though, I realised that there are no factory method that creates a mutable Clock
. I tried to create my own mutable clock, so that my tests can look like this:
private final FakeClock fakeClock = new FakeClock();
@Test
public void fooReturnsAAfterAnHour() {
var myInstance = new ClassToTest(fakeClock);
assertThat(myInstance.foo(), is("A"));
fakeClock.goForward1Hour(); // this mutates the clock, and changes what instant() will return
assertThat(myInstance.foo(), is("A"))
}
@Test
public void fooReturnsBWithinAnHour() {
var myInstance = new ClassToTest(fakeClock);
assertThat(myInstance.foo(), is("A"));
fakeClock.goForward30Minutes(); // this mutates the clock, and changes what instant() will return
assertThat(myInstance.foo(), is("B"))
}
only to find that in the documentation, it says:
All implementations that can be instantiated must be final, immutable and thread-safe.
So it seems like a mutable implementation of Clock
is incorrect. But looking up posts on stack overflow, many suggest using a mutable clock (example). Some also suggest mocking the Clock
abstract class. When I tried that out, jMock (which is what I'm using) doesn't like it because Clock
is not an interface. Apparently, if I want to mock classes, I'd have to include another dependency, which is mainly used for mocking legacy code. That doesn't sound appropriate for mocking Clock
to me.
I can also add a setter for the clock
field in ClassToBeTested
, so that I can do:
myInstance.setClock(Clock.offset(clock, Duration.ofHours(1)));
to advance the time. But I think this breaks encapsulation.
And at this point, I'm out of ideas. What can I do?
Your FakeClock
approach is fine. That's the approach I'd use.
If you prefer to use an implementation from a library instead of your own, you could use MutableClock
from the threeten-extra library. That class was designed for exactly this use case.
The "immutable" requirement was removed from the Clock
class in Java 17. It was removed not because of changes being made to Clock
's implementation, but because the requirement was harmfully restrictive (see your use case) while not serving a useful purpose. So if you're using Java <17, I'd say it's safe to simply ignore that requirement.