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.assertSoftly
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import 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.DateTime

class 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.FunSpec
import io.kotest.matchers.shouldBe
import org.joda.time.DateTime
import org.joda.time.DateTimeUtils


class 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.assertSoftly
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import org.joda.time.DateTime
import org.joda.time.DateTimeUtils


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") {
        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

Resources:

JodaOrg/joda-time
Joda-Time is the widely used replacement for the Java date and time classes prior to Java SE 8. - JodaOrg/joda-time

Get new posts directly to your inbox, subscribe to our newsletter

* indicates required

For GDPR compliance, I have to ask you to allow me to send emails to you. I will use your email address to notify you about new content on my site.

You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp's privacy practices here.