Test Fixtures
A test fixture is a known, fixed environment to use as a baseline when running tests. Typically it consists of a set of objects that are created and initialized to a known state, and then made available to every test.
Test fixtures help to eliminate code duplication by defining objects that are used by multiple tests in a single place.
Test fixtures can also speed up test execution by ensuring that time-consuming object configuration is performed once, instead of being repeated unnecessarily in multiple tests.
Task 13.2
Let’s work through an example of using a fixture in Kotest.
-
The
tasks/task13_2subdirectory of your repository is a Gradle project that implements a class representing time on a 24-hour clock.The class is defined in
Time.ktand its unit tests are in the fileTimeTest.kt. Open these files and examine them carefully. -
Run the tests, with
./gradlew testThey should all pass.
-
Note any examples of code duplication that you can see in
TimeTest.For example, in the portion of the code shown below, you can see that a
Timeobject representing a time of midnight is defined more than once.class TimeTest : StringSpec({ "Hours stored correctly" { val midnight = Time(0, 0, 0) val noon = Time(12, 0, 0) withClue("00:00:00") { midnight.hours shouldBe 0 } withClue("12:00:00") { noon.hours shouldBe 12 } } "Minutes stored correctly" { val midnight = Time(0, 0, 0) val thirtyMin = Time(0, 30, 0) withClue("00:00:00") { midnight.minutes shouldBe 0 } withClue("00:30:00") { thirtyMin.minutes shouldBe 30 } } ... }) -
Redefine
midnightoutside the tests, at the start ofTimeTest. Remove any definitions that are made inside the tests:class TimeTest : StringSpec({ val midnight = Time(0, 0, 0) "Hours stored correctly" { val noon = Time(12, 0, 0) withClue("00:00:00") { midnight.hours shouldBe 0 } withClue("12:00:00") { noon.hours shouldBe 12 } } "Minutes stored correctly" { val thirtyMin = Time(0, 30, 0) withClue("00:00:00") { midnight.minutes shouldBe 0 } withClue("00:30:00") { thirtyMin.minutes shouldBe 30 } } ... })The variable
midnightis now a test fixture. This definition is executed once by default (though see the discussion below), and the variable can be used by any of the tests defined insideTimeTest. -
Repeat this process for any other
Timeobjects that are used more than once by the tests. After moving an object into the fixture, rerun the tests to check that they all still pass.You should end up with a test fixture consisting of six
Timeobjects. This new version ofTimeTestshould be around eight lines shorter than the original.
Isolation Modes
In the example above, Kotest creates a single instance of the TimeTest
class and reuses it to run each of the specified tests. This means that the
code to create the fixture is executed only once.
This is ideal for situations like this one, where the class under test has
no methods that change object state. But what if we were testing a class
in which changes in state were possible? For example, what if Time had been
defined with var properties, allowing hours, minutes and seconds to be
altered after object creation?
This creates a potential problem. When objects are mutable, it is possible for a test to modify an object in the test fixture so that it is no longer in the state expected by other tests!
The solution to this problem is to switch Kotest’s isolation mode from ‘single instance’ (the default) to ‘instance per test’. In this mode, Kotest creates a new instance of the test spec before running each test. This means that the fixture will be recreated before each test, so it no longer matters if a test modifies the objects in the fixture.
Here’s an example of how to switch to ‘instance per test’ in a single set of tests:
import io.kotest.core.spec.IsolationMode
...
class MyTests : StringSpec({
isolationMode = IsolationMode.InstancePerTest
...
})
The Kotest documentation has more information on isolation modes, including details of how to set a project-wide default isolation mode.