
Create human-readable, Gherkin-style scenarios in code with reusable steps, shared scenario context and variants branching to run multiple scenario permutations while keeping implementation loosely coupled.
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.
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:
given / when / then), are reusable, and easy to read.hotest {}, which sets up shared scenario context between steps.@Test annotation.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.koin.get()
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 is additional feature of HOTest that further increases test readability.
It allows you to clearly present different variants of the same test scenario by:
variants).Example
@Test
fun `example of variants`() {
hotest {
`first step - it runs always as 1st for all variants`()
variants {
variant {
`this step runs only for this variant`()
`this step also runs only here`()
}
variant {
`this specialized step runs only in this variant`()
`and another step - without repeating common boilerplate`()
}
variant {
`and this runs only here`
}
}
`final step - it runs on finish of each variant`()
}
}Steps execution odrer:
// loop 1
first step - it runs always as 1st for all variants
this step runs only for this variant
this step also runs only here
final step - it runs on finish of each variant
// loop 2
first step - it runs always as 1st for all variants
this specialized step runs only in this variant
and another step - without repeating common boilerplate
final step - it runs on finish of each variant
// loop 3
first step - it runs always as 1st for all variants
and this runs only he
final step - it runs on finish of each variant
Each variant causes new execution of whole test but with only this particular variant.
For real world example look check test in this project.
How variants execute
variants {} defines a block with multiple variant {} branches.variant {} is executed in a separate run of the surrounding test.Rules for using variants
variant {} blocks inside a variants {} block.variants block is allowed at a given test level.variants can be nested inside other variants blocks, 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") { ... }
}
}Make sure your project searches for dependencies in mavenCentral().
Add the dependency:
// KMP projects
sourceSets {
commonTest.dependencies {
implementation("io.github.gt4dev:hotest:0.3.0")
..// JVM, Android projects
dependencies {
testImplementation("io.github.gt4dev:hotest:0.3.0")
..The way you write tests heavily depends on your project:
HOTest is agnostic about these details. It can be used in most cases.
It is best to review real tests from MultiProjectFocus project and follow their concepts.
There are examples of using HOTest for:
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.
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:
given / when / then), are reusable, and easy to read.hotest {}, which sets up shared scenario context between steps.@Test annotation.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.koin.get()
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 is additional feature of HOTest that further increases test readability.
It allows you to clearly present different variants of the same test scenario by:
variants).Example
@Test
fun `example of variants`() {
hotest {
`first step - it runs always as 1st for all variants`()
variants {
variant {
`this step runs only for this variant`()
`this step also runs only here`()
}
variant {
`this specialized step runs only in this variant`()
`and another step - without repeating common boilerplate`()
}
variant {
`and this runs only here`
}
}
`final step - it runs on finish of each variant`()
}
}Steps execution odrer:
// loop 1
first step - it runs always as 1st for all variants
this step runs only for this variant
this step also runs only here
final step - it runs on finish of each variant
// loop 2
first step - it runs always as 1st for all variants
this specialized step runs only in this variant
and another step - without repeating common boilerplate
final step - it runs on finish of each variant
// loop 3
first step - it runs always as 1st for all variants
and this runs only he
final step - it runs on finish of each variant
Each variant causes new execution of whole test but with only this particular variant.
For real world example look check test in this project.
How variants execute
variants {} defines a block with multiple variant {} branches.variant {} is executed in a separate run of the surrounding test.Rules for using variants
variant {} blocks inside a variants {} block.variants block is allowed at a given test level.variants can be nested inside other variants blocks, 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") { ... }
}
}Make sure your project searches for dependencies in mavenCentral().
Add the dependency:
// KMP projects
sourceSets {
commonTest.dependencies {
implementation("io.github.gt4dev:hotest:0.3.0")
..// JVM, Android projects
dependencies {
testImplementation("io.github.gt4dev:hotest:0.3.0")
..The way you write tests heavily depends on your project:
HOTest is agnostic about these details. It can be used in most cases.
It is best to review real tests from MultiProjectFocus project and follow their concepts.
There are examples of using HOTest for: