The other day I was reviewing a couple of pull request in a Java project, and I noticed that both contained a problem with the way time was being handled in the tests. Normally I would not have thought much of it, but the fact that these two problems appeared almost simultaneously in two pull requests submitted by two different developers, and that each problem was essentially the opposite of the other, prompted me to write this post.
The common theme of the two pull requests was that they both contained unit tests that used the Instant
class, and that they made assumptions about the speed at which the code runs compared to the internal
clock of the system, resulting in flaky tests.
Problem 1: Assuming that the code runs fast enough
In the first pull request, the developer had written a complex test, but in order to illustrate the problem, let’s consider a massively simplified version of the test:
public class TimeTest {
@Test
void testTimeMovesForward() {
Instant present = Instant.now();
Instant past = Instant.now().minusSeconds(60);
assertThat(past).isBefore(present);
}
}
Normally this test would pass, because the first call to Instant.now()
returns the current time,
and the second call returns a time that is 60 seconds in the past, so the assertion that past
is before present
holds true. However, this test is flaky because it assumes that the code runs fast enough so that
the second call to Instant.now()
is executed no more than 60 seconds after the first call.
That is a reasonable assumption in most cases, but it is not guaranteed to be true. For example, if we
set up a breakpoint in the line that starts with Instant past =
, and we wait for 60 seconds before
continuing the execution, the test will fail because the second call to Instant.now()
will return a time
that is actually in the future compared with present
. Another way to trigger the failure is to
add a Thread.sleep(61_000)
before the second call to Instant.now()
:
$ ./gradlew test
> Task :test FAILED
TimeTest > testTimeMovesForward() FAILED
java.lang.AssertionError at TimeTest.java:16
Breakpoints and Thread.sleep()
may seem like an extreme way to trigger the failure, but in practice there
are many other ways in which the code can sometimes run slower than expected, like garbage collection,
high CPU load, CPU trashing, or simply a slow computer. This kind of flakiness can be very hard to track down.
There are many ways to avoid this problem. One may think that the simplest solution is to
use a wider gap between present
and past
, like 10 minutes instead of 60 seconds, but that is just
a band-aid. Another solution is to avoid time subtraction altogether, and instead only use
time addition, like this:
Instant present = Instant.now();
Instant future = Instant.now().plusSeconds(60);
This may look like a reasonable solution, but it is making another assumption, namely that the code runs in a forward direction, which may not hold in every programming language or environment. For example, some languages and compilers may optimise the code in such a way that it seems to run backwards or in parallel. That is a complex topic that is beyond the scope of this post, and fortunately we do not have to worry about it in this case, because there is a much simpler solution that works in general: avoid sampling the system clock more than once! For example, we can rewrite the test like this:
public class TimeTest {
@Test
void testTimeMovesForward() {
Instant present = Instant.now(); // sampling the system clock once
Instant past = present.minusSeconds(60);
assertThat(past).isBefore(present);
}
}
That eliminates any assumption about the speed at which the code runs,
because we are only sampling the system clock once. But can we do better than that? Yes, we can!
There is no reason present
has to be the current time. We can use any time we want, completely
removing the sampling of the system clock and using only constants:
public class TimeTest {
@Test
void testTimeMovesForward() {
Instant present = Instant.EPOCH; // using a constant time
Instant past = present.minusSeconds(60);
assertThat(past).isBefore(present);
}
}
Almost any constant will do here. In the example above, I used Instant.EPOCH
, but we could have used
an arbitrary time like Instant.parse("2000-01-01T00:00:00Z")
. Why is using a constant better than
sampling the system clock even once? Because it makes the test deterministic, meaning that it will always
pass or fail in the same way, regardless of when it is executed. For example, with the previous version of the test
that sampled the system clock once, the test would pass if executed in our lifetime, but it would fail due to
overflow if executed
near the Instant.MIN
, which is the lower bound of the Instant
class range. In case you are wondering when
that was, it was a billion years ago, well before humans invented computers. But if some single-celled
organism living in the ocean of the primitive Earth or the supercontinent Rodinia were to run the test, it would fail for them.
That may seem like an extreme case, but a more complex and realistic test may have a much narrower range of
validity.
For those reasons, every time I see a call to Instant.now()
in a test, I immediately start suspecting that
the test may be flaky, and I start thinking about how to eliminate that call and use a constant time instead.
Problem 2: Assuming that the code runs slowly enough
The second pull request was representative of the opposite problem to the one described above. In order
to understand this second problem, let’s assume we have a class of Widget
s, and a WidgetRepository
that stores them. For the sake
of simplicity, the widgets are stored in a Map
in memory:
public class Widget {
private final Long id;
private Instant creationTime;
private Instant lastModifiedTime;
public Widget(Long id) {
this.id = id;
}
// Getters and setters omitted for brevity
}
public class WidgetRepository {
private final Map<Long, Widget> widgets = new ConcurrentHashMap<>();
public void addWidget(Widget widget) {
Instant now = Instant.now();
widget.setCreationTime(now);
widget.setLastModifiedTime(now);
widgets.put(widget.getId(), widget);
}
public void updateWidget(Widget widget) {
widget.setLastModifiedTime(Instant.now());
widgets.put(widget.getId(), widget);
}
public Widget getWidgetById(Long id) {
return widgets.get(id);
}
}
And here a test that checks that the lastModifiedTime
is updated when a widget is updated:
class WidgetRepositoryTest {
private static final long ID = 1L;
@RepeatedTest(3)
void shouldUpdateWidget() {
WidgetRepository repository = new WidgetRepository();
repository.addWidget(new Widget(ID));
Widget createdWidget = repository.getWidgetById(ID);
assertThat(createdWidget).isNotNull();
assertThat(createdWidget.getCreationTime()).isNotNull();
assertThat(createdWidget.getLastModifiedTime()).isEqualTo(createdWidget.getCreationTime());
// pretend the widget is modified, and now we update it
repository.updateWidget(createdWidget);
Widget updatedWidget = repository.getWidgetById(ID);
assertThat(updatedWidget.getLastModifiedTime()).isAfter(createdWidget.getCreationTime());
}
}
This seems straightforward. Note the last assertion crucially uses a strict comparison (isAfter
) in order to
confirm that the updateWidget
method has, indeed, updated the lastModifiedTime
, so the two times are
no longer equal.
However, the test is flaky because it assumes that the computer is slow enough so that the system clock
has ticked at least once between the two calls to Instant.now()
, namely the one in addWidget
and the one
in updateWidget
. This test will fail if the computer is fast enough that the two calls to Instant.now()
return the same value.
Java’s Instant
class has a precision of nanoseconds, which means that it can represent time with a resolution of
one nanosecond. As of 2025, even modest computers can execute billions of instructions per second, which means that
it is entirely possible for two calls to Instant.now()
to return the same value if they are executed in quick
succession. And future computers will likely be even faster, making this problem more pronounced. Which means that
this test may pass reliably in a slow computer, but it will eventually fail when computers become fast enough.
It’s a ticking time bomb (pun intended).
To make things worse, even if Instant
representws time with nanosecond precision,
the actual precision of the system clock is likely to be lower than that. How low? It depends on the hardware,
the operating system and the JVM version. For example, versions of Windows prior to Windows 8 used to have a
precision of 15 milliseconds, which is ample time for a fast computer to execute many instructions.
Similarly, older versions of the JVM had a precision of milliseconds, which means the test may pass on a new version of the JVM, but fail on an older version. This is exactly what happens in my computer. When I run the test using Java 21, it seems to pass every time (although it cannot be guaranteed):
$ jenv local 21
$ ./gradlew test
...
BUILD SUCCESSFUL in 1s
but if I run it using Java 8, it seems to fail every time:
$ jenv local 1.8
$ ./gradlew test
> Task :test FAILED
WidgetRepositoryTest > shouldUpdateWidget() > repetition 2 of 3 FAILED
java.lang.AssertionError at WidgetRepositoryTest.java:24
WidgetRepositoryTest > shouldUpdateWidget() > repetition 3 of 3 FAILED
java.lang.AssertionError at WidgetRepositoryTest.java:24
3 tests completed, 2 failed
Note the use of @RepeatedTest(3)
to run the test three times, which is a good practice to
catch flaky tests. Looks like the first repetition passes, but the second and third ones fail,
because they run “too fast” for the system clock to tick between the two calls to Instant.now()
.
Fixing this test to make it not flakey but at the same time verify that updateWidget
does indeed
update the lastModifiedTime
is not trivial. One bad way to fix it would be to add a Thread.sleep(...)
before the call to updateWidget
, to ensure the system clock has ticked at least once, but for
a number of reasons that is not a good idea. Another idea would be to add a retry loop using
a library like Awaitility to wait for the lastModifiedTime
to be different from the
creationTime
, but again that comes with its own set of problems.
The proper fix is to decouple the code from the real time, replacing the static calls to Instant.now()
with a
Clock
that can be controlled by the test. This way, we can simulate the passage of time without
making assumptions about the speed of the code or the precision of the system clock.
The details of how to do that are enough for a separate post.
Conclusion
Writing tests that use time can be tricky, because they can easily become flaky if we make assumptions about the speed at which the code runs or the precision of the system clock. The main takeaways from this post are:
-
Avoid sampling the system clock more than once in a test, and ideally use constants instead of
Instant.now()
. -
Do not assume that the clock will tick between two calls to
Instant.now()
because it may not do so if the code runs fast enough compared to the precision of the clock. -
Avoid static calls to
Instant.now()
in production code, because it makes the code harder to test. Instead, use an injectableClock
that can be controlled by the tests.