fbpx

Testing time-based code with Joda Time

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:

https://github.com/JodaOrg/joda-time


more insights

Uncategorized
Jarosław Michalik

#kotlinDevChallenge 2023 summary

The KotlinDevChallenge was not just a competition, but a journey of learning, growth, and community engagement. Every participant in this challenge is a winner in

Read More »
KotlinDevChallenge2023
Jarosław Michalik

#kotlinDevChallenge – 24

What do you love most about Kotlin? Productivity increase? Idioms? Or that it isn’t Java? Maybe something else! Share it. It can be short. It

Read More »

AndroidPro newsletter 🚀

join 3057 developers reading AndroidPro newsletter every week

👉 tips & tricks, articles, videos

👉 every Thursday in your inbox

🎁 bonus on signup – Clean Architecture blueprint

brought to you by Jarek Michalik, Kotlin GDE

You can unsubscribe any time. For details visit our privacy policy.

android pro newsletter tips and tricks how to learn android