NOTE: HOTest is under heavy development.
The library implementation is at an early stage and the API is changing, but the pattern is stable.
HOTest is a testing pattern and library that blends Gherkin-like readability with xUnit-style tests.
You write scenarios in human-friendly language, but still in code, which keeps tests close to the implementation while avoiding tight coupling of tests and production.
- Focus on creating human-readable tests but still in Kotlin.
- Reusable test steps make writing tests much easier.
- Business rules are expressed in the most human-friendly way, with minimal technical details.
- Readability of specification / tests have the highest priority.
- Readable specification / tests:
- make changes in business logic easier
- enable better understanding of business opportunities, leading to business evolutions.
- In HOTest, business logic is the top driver; implementation is a technical detail.
- You can truly start with tests (TDD) or specification (BDD) and later derive production code from it.
- Loose coupling between tests and production code prevents architecture from becoming rigid.
- You can change architecture of the solution, even shift programming paradigms, still keeping business rules untouched.
- You can easily change implementation details without changing business rules.
- Clearer architecture through separation between what should happen (test) and how it happens (production code).
- Reusable steps speed up writing new scenarios and make exploring many variants of a scenario easier.
When tests must verify low-level implementation details or exact API calls.
It's worth using HOTest only when you have human-readable business requirements.
Sample test:
@Test
fun `exchange currencies - direct rate use`() {
hotest {
`given fake rates service returns`(
ExchangeRate("EUR", "PLN", 4),
ExchangeRate("EUR", "CHF", 2),
ExchangeRate("EUR", "USD", 1),
)
`when exchange calculator converts`(
Money(10, "EUR"),
Currency.PLN
)
`then exchange calculator returns`(
Money(40, "PLN"),
)
}
}
@Test
fun `exchange currencies - reversed rate use`() {
hotest {
`given fake rates service returns`(
ExchangeRate("EUR", "PLN", 4),
ExchangeRate("EUR", "CHF", 2),
ExchangeRate("EUR", "USD", 1),
)
`when exchange calculator converts`(
Money(40, "PLN"),
Currency.EUR
)
`then exchange calculator returns`(
Money(10, "EUR"),
)
}
}Notes about the example:
- Steps express intent, not implementation. Tests say what should happen, not which methods to call.
- Steps use human-language names (Gherkin-style
given / when / then), are reusable, and easy to read. - Steps are called inside
hotest {}, which sets up shared scenario context between steps. - The test uses a standard
@Testannotation.
Any test framework can run HOTest scenarios: JUnit, Kotest, Kotlin test, etc.
Sample step definition:
fun HOTestCtx.`then exchange calculator returns`(
money: Models.Money,
) {
val result: Money = this[KEY_RESULTS]
Assertions.moneyEquals(money, result)
}To keep context between step calls, all steps are called in the same context HOTestCtx.
HOTestCtx stores SUT objects and all data required by the scenario and shared between steps.
variants {} reduce boilerplate when you want several scenario variations without duplicating shared parts.
How variants execute
variants {}defines a block with multiplevariant {}branches.- Each
variant {}is executed in a separate run of the surrounding test. - Traversal order follows depth-first search (graph's DFS algorithm) through nested variants.
Example
hotest {
println("step on start")
variants {
variant { println("variant1") }
variant { println("variant2") }
variant { println("variant3") }
}
println("step on end")
}Output:
// 1st loop
step on start
variant1
step on end
// 2nd loop
step on start
variant2
step on end
// 3rd loop
step on start
variant3
step on end
Each variant causes new execution of whole test but with only this particular variant.
Rules for using variants
- You can define any number of
variant {}blocks inside avariants {}block. - Only one
variantsblock is allowed at a given test level. variantscan be nested inside othervariantsblocks, as shown below:
// example of nested variants
hotest {
// call steps common for ALL variants
// ...
// 1st level of variants
variants("variants for different nutrition") {
variant("proteins") {
// call steps common in "proteins" related scenarios
// 2nd level of variants
variants("variants for different proteins") {
variant("proteins from vegetables") {
// call steps related only to this variant
}
variant("proteins from meat") {
// call steps for this variant
}
}
}
variant("fats") { ... }
variant("carbs") { ... }
}
}For advanced, full examples of HOTest usage, refer to the Multi Project Focus project.
- Sample tests: main/composeApp/src/commonTest
- HOTest integration with your project:
Temporary integration uses Gradle composite build.