Skip to content

Instantly share code, notes, and snippets.

@delitescere
Created September 28, 2022 09:07
Show Gist options
  • Save delitescere/9a6103be5af6ef3253e4a99ef37801c0 to your computer and use it in GitHub Desktop.
Save delitescere/9a6103be5af6ef3253e4a99ef37801c0 to your computer and use it in GitHub Desktop.
A JUnit5 base test class for Kotlin that includes TestInfo values for data partitioning and Clock manipulation for time-sensitive tests
package test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInfo
import org.junit.jupiter.api.TestInstance
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import java.util.*
import kotlin.math.absoluteValue
import kotlin.properties.Delegates
/**
*
* To set a clock that doesn't tick for the whole test class (unless a test case makes it tick), do something like this:
*
* ```kotlin
* private val clock: Clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
* internal class TestSomething : BaseTest(clock) {
* }
* ```
* The clock will be fixed to the time the test class is statically initialized.
*
*
* or:
*
* ```kotlin
* internal class TestSomething : BaseTest() {
* init { epochUtcClock() }
* }
* ```
* using a helper function when the test class is instantiated, which will be sensitive to whatever [TestInstance.Lifecycle] is in effect.
*
* or, to ensure the clock is reset before each test case, regardless of Lifecycle:
*
* ```kotlin
* internal class TestSomething : BaseTest() {
* @BeforeEach
* fun fixClock() = epochUtcClock()
* }
* ```
*
* @param clock A clock (e.g. [Clock.fixed]) for time-sensitive testing. Defaults to the "normal" clock [Clock.systemDefaultZone].
* Helper functions can set this clock for each test. It is reset to the originally specificied clock before each test.
* @property testId A positive integer used for data partitioning for each test case.
*/
@Suppress("MemberVisibilityCanBePrivate")
abstract class BaseTest(clock: Clock? = null) {
private val _originalClock: Clock = clock ?: Clock.systemDefaultZone()
private var _clock = _originalClock
private var _testNow = Instant.now(_clock)
private var _testId by Delegates.notNull<Int>()
private var _testName by Delegates.notNull<String>()
/**
* A deterministic numerical identity for the test case. It will be a low positive integer, and stable for as long as the following remain unchanged:
* - The test class name
* - The test case name (i.e. the test method name)
* - The number of test cases in a test class
*/
protected val testId
get() = _testId
protected val testName
get() = _testName
protected val testNow
get() = _testNow
protected val clock
get() = _clock
@BeforeEach
fun useTestInfo(info: TestInfo) {
_testId = (info.testMethod.get().hashCode() % (info.testClass.get().methods.size)).absoluteValue + 1
_testName = upperCamelCase(info.testMethod.get().name)
}
private fun upperCamelCase(s: String): String {
val words = s.split(Regex("[\\W_]+"))
val builder = StringBuilder()
for (i in words.indices) {
val word: String = words[i]
builder.append(word[0].uppercaseChar().toString() + word.substring(1).lowercase(Locale.getDefault()))
}
return builder.toString()
}
/**
* Fix the clock to the current time in UTC
*/
protected fun utcClock() {
_clock = Clock.fixed(Instant.now(_originalClock), ZoneId.of("Z"))
}
/**
* Fix the clock to Epoch in the current zone
*/
protected fun epochClock() {
_clock = Clock.fixed(Instant.EPOCH, _originalClock.zone)
}
/**
* Fix the clock to the current time in the current zone
*/
protected fun fixedClock() {
_clock = Clock.fixed(Instant.now(_originalClock), _originalClock.zone)
}
/**
* Fix the clock to Epoch in UTC
*/
protected fun epochUtcClock() {
_clock = Clock.fixed(Instant.EPOCH, ZoneId.of("Z"))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment