Leonardo Colman Lopes
Posted on April 16, 2020
TL;DR - How to use Kotest 4.0.0 new Test Factory feature to include reusable blocks of test in your test suite
Hello, Kotliners!
In this article we'll see how to use a feature from the Kotest Framework to create reusable tests in your test suite, for cases in which you test multiple classes exactly the same way.
Declaring a Test Factory function
It's very easy to create a reusable test. You must first pick your favourite style among Kotest's test styles. You can pick any of them, but my personal favourite (and the one I'll use here) is FunSpec
.
Declaring a test using FunSpec
is as simple as:
class MySpec : FunSpec({
test("Foo should not be equal to bar") {
"foo" shouldNotBe "bar"
}
})
Declaring the same test, but this time as a test factory is very similar:
// Top level val
val myReusableTest = funSpec {
test("Foo should not be equal to bar") {
"foo" shouldNotBe "bar"
}
}
However, there are some cases in which having the same test will produce a different result if called multiple times. For cases which our tests depend on some parameters, we might rather want to use use this as a function:
fun myReusableTest(myParameter: String) = funSpec {
test("$myParameter should not be equal to bar") {
myParameter shouldNotBe "bar"
}
}
Both cases can be included in any Spec
by using the include
function:
class MyTest : FunSpec({
test("My usual test") {
// ...
}
include(myReusableTest("A"))
include(myReusableTest("B"))
test("Another usual test") {
// ...
}
})
And as expected, the tests from the test factory are applied!
A small example
Now that we understand the basic concept, let's play a little bit with a more realistic example: Calculators! really realistic, everyone implements calculators
Let's suppose we have 3 types of calculators:
- A Sum Calculator
- A Subtraction Calculator
- A Complete Calculator, that can do both operations.
interface SumOp {
fun sum(a: Int, b: Int): Int
}
object SumCalculator : SumOp {
override fun sum(a: Int, b: Int) = a + b
}
interface SubtractionOp {
fun subtract(a: Int, b: Int): Int
}
object SubtractionCalculator : SubtractionOp {
override fun subtract(a: Int, b: Int) = a - b
}
To test these implementations, we would create a Spec
for each:
class SumCalculatorTest : FunSpec({
test("Sum 2 and 2 should be 4") {
SumCalculator.sum(2, 2) shouldBe 4
}
})
class SubtractionCalculatorTest : FunSpec({
test("4 minus 2 should be 2") {
SubtractionCalculatorTest.subtract(4, 2) shouldBe 2
}
})
When implementing our CompleteCalculator
, we should test these behaviours as well.
object CompleteCalculator : SumOp, SubtractionOp {
override fun sum(a: Int, b: Int) = a + b
override fun subtract(a: Int, b: Int) = a - b
}
class CompleteCalculatorTest : FunSpec({
test("Sum 2 and 2 should be 4") {
CompleteCalculator.sum(2, 2) shouldBe 4
}
test("4 minus 2 should be 2") {
CompleteCalculator.subtract(4, 2) shouldBe 2
}
})
We have ended up writing the same test that we already wrote for sum calc and sub calc. This is duplication that we'd like to try and avoid.
Now let's exercise what we learned previously. Writing tests through composition:
fun sumTests(calc: SumOp) = funSpec {
test("Sum 2 and 2 should be 4") {
calc.sum(2, 2) shouldBe 4
}
}
fun subtractionTests(calc: SubtractionOp) = funSpec {
test("4 minus 2 should be 2") {
calc.subtract(4, 2) shouldBe 2
}
}
class SumCalculatorTest : FunSpec({
include(sumTests(SumCalculator))
})
class SubtractionCalculatorTest : FunSpec({
include(subtractionTests(SubtractionCalculator))
})
class CompleteCalculatorTest : FunSpec({
include(sumTests(CompleteCalculator))
include(subtractionTests(CompleteCalculator))
})
And voilà! We successfully reused a test for more than one scenario!
What could this be useful for?
In the real world we won't be implementing calculators as often as we want to. So, why would I use this kind of factory and composition?
We created these Test Factories for any kind of repeatable test pattern. For example:
- If you need to test that every
Repository<MyEntity>
creates a database entity with a new ID - If your
cache
and your actual database return data in the same format - If sending a
POST
request to some endpoint always returns the payload in a specific format - If all instances of a
sealed class
have their class' name on thetoString
function
And much more scenarios that you might be exploring with!
Posted on April 16, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.