DEV Community

loading...
Kotest

Writing reusable tests with Kotest's Test Factory

Leonardo Colman Lopes
Kotlin enthusiast, enjoys open source as a hobby and is really into software testing!
Updated on ・3 min read

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:

  1. A Sum Calculator
  2. A Subtraction Calculator
  3. 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 the toString function

And much more scenarios that you might be exploring with!

Discussion (0)