Mocking java.time.Instant.now() in PlayFramework

It gets a bit tricky to make the production code clean while making it convenient to mock a static method in a multi-threaded setting since Java and Scala don't support "monkey-patching" like Ruby, Python, and Javascripts.

Mocking time is one of the mechanisms implemented inside PlayFast (an opinionated PlayFramework template). I plan to publish more blogs in order to document the decisions and also serve as documentation for PlayFast.

SPONSORED

PlayFast is an opinionated dev-ready production-ready PlayFramework template. This includes hot-reloadable Svelte + TailwindCSS, Postgres with Enum support, Docker-compatible deployment (e.g. Dokploy, Coolify, Render, Heroku), JobRunr (background processing), and browser testing. An example is provided for every capability.

It is built for me to start a new project faster. You can make it yours by adding, configuring, and remove capabilities.

Learn more

A while ago, when I was making PlayFast, I needed to mock java.time.Instant.now(). Controlling time in test is quite important for making the test stable and non-flaky.

Here are the considerations:

  1. Easy to use in the production code: it should not change the code structure too much.
  2. Work in a multi-threaded setting, particularly in browser testing and background processing: there are a running PlayFramework server and a JobRunr server that spawn multiple threads for its workers.

I've come up with 3 solutions

  1. Make our own mockable/stub-able Instant class and ban java.time.Instant from the codebase.
  2. Inject a Clock instance everywhere and inject a mock Clock instance in test.
  3. Use Mockito to mock Instant.now()

Let's look deeper into each solution one by one.

Make our own mockable Instant

We can make our own framework.Instant and use it in place of java.time.Instant as shown below:

package framework

import java.time.temporal.ChronoUnit

type Instant = java.time.Instant

object Instant {
  private[this] var mockedTime: Option[Instant] = None

  def mockTimeForTest(t: Instant): Unit = {
    mockedTime = Some(t)
  }

  def now(): Instant = mockedTime.getOrElse(java.time.Instant.now())
}

Then, we switch every java.time.Instant.now() to framework.Instant.now().

In test, we can control time using framework.Instant.mockTimeForTest(...)

To make this approach more robust, we can use Scalafix to ban java.time.Instant.now() from the codebase using scalafix-forbidden-symbol.

com.twitter.util.Time uses a similar approach to support controlling time in test but is more complex and powerful.

Inject a Clock instance everywhere

We can look at time as an external service and inject it.

💡
You cannot inject java.time.Clock directly because it is immutable and you wouldn't be able to advance time. Wrapping it under a ClockService would allow that.

First, we will need to make a ClockService with a ClockModule as shown below:

package modules

import com.google.inject.AbstractModule

import java.time.{Clock, Instant}

class ClockService {
  var clock: Clock = Clock.systemUTC()

  def useFixedClock(fixed: Clock): Unit = {
    clock = fixed
  }

  def now(): Instant = {
    Instant.now(clock)
  }
}

class ClockModule extends AbstractModule {
  override def configure(): Unit = {
    bind(classOf[ClockService]).toInstance(new ClockService)
  }
}

Then, we tell PlayFramework to instantiate the module at the start in application.conf:

play.modules.enabled += "modules.ClockModule"

Then, in your controller or any place that you want to get a time, you can inject ClockService in as shown below:

@Singleton
class HomeController @Inject() (
  clockService: ClockService
)(implicit ec: ExecutionContext) {

  def index() = async() { implicit req =>
    println("In controller: " + clockService.now())
    ...
  }
}

In test, you can use clockService.useFixedClock(..) to control time.

This is very similar to the first approach. ClockService serves the same role as framework.Instant except that it is injected instead of being globally accessible.

This approach also requires banning java.time.Instant from the codebase as well, and you can use scalafix-forbidden-symbol to achieve that.

Here's a working example with PlayFramework: https://github.com/tanin47/playfast/pull/15

Use Mockito to mock Instant.now()

If this worked with multi-threads, it would have been the best solution. because it wouldn't require adjusting the production code just to accommodate testing.

Unfortunately, mocking a static method only applies in its own thread. Other threads in a PlayFramework server or JobRunr instance won't work. Here's the proof of concept.

We could make it work by mocking time in every current thread and threads that will be created in the future.

Technically, it might be possible by iterating through all alive threads and inject the mocking code. For future threads, we can use ByteBuddy to inject the mocking code into the beginning of Thread.run(). This solution is too hacky for my taste because it would modify byte code directly, so I've decided against it.

Just in case anyone is interesting, here's a working snippet for mocking Instant.now() or any a static method:

val fixedInstant = Instant.parse("2024-11-01T10:00:00Z")
val mockedInstant = mockStatic(classOf[Instant], Mockito.CALLS_REAL_METHODS)
when(Instant.now()).thenReturn(fixedInstant)

Instant.now() should be(fixedInstant)

One thing to note is that I suspect Mockito also modifies byte code and/or uses a low-level API to mock classes and objects. Since we don't see nor maintain it and it's battle-tested enough, it's kinda okay to use Mockito. It's an out-of-sight-out-of- mind thing.

Until Mockito supports mocking static methods that apply to all threads, this solution is deemed infeasible.

Conclusion

Eventually, I've decided to go Number 1 (Make our own mockable Instant) because it is the least verbose solution and centralizes the test-related code to a single file: framework/Instant.scala.

Number 2 is similar to Number 1 in almost every aspect except that ClockService needs to be injected everywhere, and this would make the code a lot more verbose. In my codebase, this would mean modifying 100+ method signatures; I'd rather not do that.

Being told to use X instead of Y is also nothing new in the Java world. There are many classes we shouldn't use in Java e.g. java.util.Date and java.text.SimpleDateFormat. Thinking about it, these should be banned using scalafix-forbidden-symbol as well.

Subscribe to tanin

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe