TimeFreezer: Kotlin time mocking library inspired by FreezeGun
TimeFreezer makes you test time related code with LocalDate.now()
/ LocalDateTime.now()
based on MockK the mocking library for Kotlin.
This library is published by JitPack, so you have to add JitPack Maven repository.
// build.gradle.kt
repositories {
...
maven { url = uri("https://jitpack.io") }
}
dependencies {
...
testImplementation("com.github.ivvve:time-freezer:${TimeFreezerVersion}")
}
You can fix the time using devson.timefreezer.freezeTime
function.
See the example code below.
import devson.timefreezer.freezeTime
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.booleans.shouldBeFalse
import io.kotest.matchers.booleans.shouldBeTrue
import java.time.LocalDate
import java.time.LocalDateTime
internal class CouponExample : FreeSpec({
"Coupon example" {
val sut = Coupon(
canUseFrom = LocalDate.of(2023, 1, 1),
expiresAt = LocalDateTime.of(2023, 1, 3, 22, 0, 0),
)
// before date can use the coupon
freezeTime(LocalDate.of(2022, 12, 31)) {
sut.isAvailable().shouldBeFalse()
sut.isAfterCanUseFrom().shouldBeFalse()
sut.isExpired().shouldBeFalse()
}
// during date can use the coupon
freezeTime(LocalDateTime.of(2023, 1, 1, 0, 0, 0)) {
sut.isAvailable().shouldBeTrue()
sut.isAfterCanUseFrom().shouldBeTrue()
sut.isExpired().shouldBeFalse()
}
// after coupon expired
freezeTime(LocalDateTime.of(2023, 1, 3, 23, 0, 1)) {
sut.isAvailable().shouldBeFalse()
sut.isAfterCanUseFrom().shouldBeTrue()
sut.isExpired().shouldBeTrue()
}
}
})
private data class Coupon(
val canUseFrom: LocalDate,
val expiresAt: LocalDateTime,
) {
fun isAvailable(): Boolean = (this.isAfterCanUseFrom()) && this.isExpired().not()
fun isAfterCanUseFrom(): Boolean = (this.canUseFrom <= LocalDate.now())
fun isExpired(): Boolean = (LocalDateTime.now() > this.expiresAt)
}
You can also use devson.timefreezer.freezeTime
function nested form.
import devson.timefreezer.freezeTime
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
import java.time.LocalDate
import java.time.LocalDateTime
internal class NestedFormExample : FreeSpec({
"Nested form example" {
freezeTime(LocalDateTime.of(2023, 1, 2, 3, 4, 5)) {
LocalDate.now().shouldBe(LocalDate.of(2023, 1, 2))
LocalDateTime.now().shouldBe(LocalDateTime.of(2023, 1, 2, 3, 4, 5))
freezeTime(LocalDateTime.of(2023, 2, 3, 4, 5, 6)) {
LocalDate.now().shouldBe(LocalDate.of(2023, 2, 3))
LocalDateTime.now().shouldBe(LocalDateTime.of(2023, 2, 3, 4, 5, 6))
}
LocalDate.now().shouldBe(LocalDate.of(2023, 1, 2))
LocalDateTime.now().shouldBe(LocalDateTime.of(2023, 1, 2, 3, 4, 5))
}
}
})
On JDK 16 and above you can run into IllegalAccessException
/ InaccessibleObjectException
when you use LocalDate
or LocalDateTime
methods like LocalDate.of(2023, 1, 15)
.
It's because of strong module encapsulation of JDK 16 and because TimeFreeze is based on MockK.
You can bypass this issue adding JVM argument --add-opens java.base/java.time=ALL-UNNAMED
.
Example for Gradle users:
// build.gradle.kt
tasks.test {
useJUnitPlatform()
// to resolve mockk issue caused by Java module system (reference: https://github.com/mockk/mockk/blob/master/doc/md/jdk16-access-exceptions.md)
jvmArgs(
"--add-opens", "java.base/java.time=ALL-UNNAMED",
)
}