The ability to quickly rerun the same test over and over with a predefined set of inputs and expected values is often called data driven testing, table driven testing or table driven property checks. Whatever name you prefer to use, Kotest has excellent support for this paradigm and this article will cover how to write such tests and introduce some of nice extra features.

Let’s start by assuming we want to test a max function — the maximum of two integers — then the old school way would be to invoke the function multiple times inline.

"maximum of two numbers" {

Math.max(2, 3) shouldBe 3

Math.max(0, 0) shouldBe 0

Math.max(4, -1) shouldBe 4

Math.max(-2, -1) shouldBe -1

}

This does work, but if we want to test many dozens of inputs or use many arguments, then it’s a bit long winded. It also becomes unwieldy when the test logic is more complicated than a one-liner. Data-driven-testing allows us to pull the data out of the test logic.

The main abstraction is the dataset which contains your inputs and the expected outputs. This is often referred to as the table, hence why this paradigm is often also called table driving testing. Each set of data for one instance of the test is usually called a row.

To get started, you must have added kotest-assertions to your build. You can always find the latest version on maven central.

There are two ways to structure these tests in Kotest. The first is to define tables upfront using the table function, explicitly naming each parameter using a header function, and wrapping each row of data inside a call to the row function.

For example, let’s rework the max test from earlier to use these functions.

"maximum of two numbers" {

table(

headers("a", "b", "max"),

row(2, 3, 3),

row(0, 0, 0),

row(4, -1, 4),

row(-2, -1, -1)

).forAll { a, b, max ->

Math.max(a, b) shouldBe max

}

}

So here you can see that our table is defined with three args — a, b and max — and then we define four rows of data. The first and second parameters are the inputs — the a and b in this case — and the last parameter we use as the expected output, the max.

After the call to setup the table, we need to provide a lambda test function which will do the job of testing each row as the values are fed in. There are two options — we can use either forAll which asserts that every row of input passes, or forNone which assert that every row of input fails.

That’s all there is to it, we’ve written a test that will run with four sets of data. In this case, you might think, gee, this is more code than the earlier copy-n-paste-the-same-line and you’d be right. But let’s look at a more complicated example where the test logic would span over several lines.

"pixel extraction example" {

table(

headers("path", "x", "y", "r", "g", "b"),

row("space.jpg", 1, 2, 255, 0, 0),

row("space.jpg", 0, 0, 255, 255, 0),

row("worldcup.jpg", 23, 2, 17, 84, 221),

row("mountain.jpg", 67, 825, 0, 0, 0),

row("piano.jpg", 845, 53, 255, 0, 46),

row("sunshine.jpg", 14, 423, 155, 65, 37)

).forAll { path, x, y, r, g, b ->



// load image from resources

val image = ImageIO.read(javaClass.getResourceAsStream(path))



val rgb = image.getRGB(x, y)



// shift 16 to get red

rgb shr 16 and 0x000000FF shouldBe r



// shift 8 to get green

rgb shr 8 and 0x000000FF shouldBe g



// just mask to get blue

rgb and 0x000000FF shouldBe b

}

}

In this example, we are pulling out pixel values from a predefined image, then testing the RGB values for those pixels. In a test like this, the input set could end up being significant.

Also, the test logic is several lines long. Of course, the test could move into another function which we could then invoke. But then you’d have to write some code to loop over each row, invoking the test function, reporting errors, and then you’ve just reinvented data driven testing.