In some systems you sometimes need to record date or timestamp of given action. Good example could be comments system, where each comment has timestamp:
interface CommentsInteractor { fun createComment(content: String): CommentEntity}data class CommentEntity( val content: String, val timestamp: Long)
In our experiment we will perform two test cases:
- comment created at given time should have given timestamp
- second comment, created one after 1 second should have proper proper timestamp.
So how can we manipulate time in unit test?
Inject time provider to system under test
That’s probably the most desirable way to achieve testability of system under test – using dependency inversion. In this particular case, we inject current time provider to CommentsInteractor via constructor:
class InjectedTimeProviderInteractor(private val provideTime: () -> Long) : CommentsInteractor { override fun createComment(content: String): CommentEntity { return CommentEntity( content = content, timestamp = provideTime() ) }}
Each provideTime() invocation will return current time in milliseconds.
Test becomes fairly simple – current time is provided via function passed in constructor – we can put any value we want and make assertions based on given fixed time:
import io.kotest.assertions.assertSoftlyimport io.kotest.core.spec.style.FunSpecimport io.kotest.matchers.shouldBeimport java.util.*class CommentsInteractorTest : FunSpec({ var currentTime: Long = Date().time val interactor: CommentsInteractor = InjectedTimeProviderInteractor { currentTime } test("fixed time: now") { val givenFixedTime = currentTime val comment = interactor.createComment("comment") comment.timestamp shouldBe givenFixedTime } test("create two comments with 1000ms delay") { val givenFixedTime = currentTime val firstComment = interactor.createComment("first comment") currentTime += 1000 val secondComment = interactor.createComment("second comment") assertSoftly { firstComment.timestamp shouldBe givenFixedTime secondComment.timestamp shouldBe givenFixedTime + 1000 } }})
Joda Time
In Android world, where developers are stuck to Java 8 (and below API 26 there is no proper java.time support), there is tendency to use JodaTime as base date and time manipulation library.
When there is no option to inject time provider or refactoring the whole class is no worth it, you may decide to use helper methods from Joda Time to achieve testable time-based code:
import org.joda.time.DateTimeclass JodaTimeBasedCommentsInteractor : CommentsInteractor { override fun createComment(content: String): CommentEntity { return CommentEntity( content = content, timestamp = DateTime.now().millis ) }}
setCurrentMillisFixed() in Joda Time
With Joda Time we can use static method setCurrentTimeMillisFixed(), which will tell Joda objects to always return given value.
import io.kotest.core.spec.style.FunSpecimport io.kotest.matchers.shouldBeimport org.joda.time.DateTimeimport org.joda.time.DateTimeUtilsclass CommentsInteractorTest : FunSpec({ val interactor: CommentsInteractor = JodaTimeBasedCommentsInteractor() val givenTime = DateTime("2020-08-11T12:00:00.000+02:00") context("fixed time: 2020-08-11T12:00:00.000+02:00") { DateTimeUtils.setCurrentMillisFixed(givenTime.millis) test("create comment with given timestamp") { val commentEntity = interactor.createComment("content") commentEntity.timestamp shouldBe givenTime.millis } }})
Provide fake time with MillisProvider in Joda Time
According to documentation:
setCurrentMillisProvider(MillisProvider millisProvider)
* Sets the provider of the current time to class specified.
* This method changes the behaviour of currentTimeMillis().
* Whenever the current time is queried, the specified class will be called.
If we want to advance time in test, we could always use several setCurrentMillisFixed calls, nevertheless there exists more fluent method – set setCurrentMillisProvider:
import io.kotest.assertions.assertSoftlyimport io.kotest.core.spec.style.FunSpecimport io.kotest.matchers.shouldBeimport org.joda.time.DateTimeimport org.joda.time.DateTimeUtilsclass CommentsInteractorTest : FunSpec({ val interactor: CommentsInteractor = JodaTimeBasedCommentsInteractor() val givenTime = DateTime("2020-08-11T12:00:00.000+02:00") test("create two comments with 1000ms delay") { var currentTime = givenTime.millis DateTimeUtils.setCurrentMillisProvider { currentTime } val firstComment = interactor.createComment("first comment") currentTime += 1000 val secondComment = interactor.createComment("second comment") assertSoftly { firstComment.timestamp shouldBe givenTime.millis secondComment.timestamp shouldBe givenTime.millis + 1000 } }})
We can also extract millis provider configuration to external function and implement our own mechanism to manipulate time in tests:
interface AdvanceTime { fun advanceTimeBy(millis: Long)}fun interactiveTime(startTime: DateTime, action: AdvanceTime.() -> Unit) { var givenMillis = startTime.millis DateTimeUtils.setCurrentMillisProvider { givenMillis } val advanceTime = object : AdvanceTime { override fun advanceTimeBy(millis: Long) { givenMillis += millis } } action(advanceTime) DateTimeUtils.setCurrentMillisSystem()}
And refactor test case to use our new mechanism:
class CommentsInteractorTest : FunSpec({ val interactor: CommentsInteractor = JodaTimeBasedCommentsInteractor() val givenTime = DateTime("2020-08-11T12:00:00.000+02:00") test("create two comments with 1000ms delay") { interactiveTime(givenTime) { val firstComment = interactor.createComment("first comment") advanceTimeBy(1000) val secondComment = interactor.createComment("second comment") assertSoftly { firstComment.timestamp shouldBe givenTime.millis secondComment.timestamp shouldBe givenTime.millis + 1000 } } }})
With this method we end up with DSL-like test code which separates what from how. Other tests may now use interactiveTime function as long as there is JodaTime used under the hood.
What are the options for testing time based code?
- inject time providers directly into system under test
- use library such as JodaTime which allows changing time providers in runtime
- use fixed time @Rules in JUnit4 or @Extension in JUnit5
- mock static system methods with Mockk, or tools like PowerMock