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.
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.
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:
- Easy to use in the production code: it should not change the code structure too much.
- 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
- Make our own mockable/stub-able
Instantclass and banjava.time.Instantfrom the codebase. - Inject a
Clockinstance everywhere and inject a mockClockinstance in test. - 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.
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.