Skip to content

Latest commit

 

History

History
355 lines (273 loc) · 11 KB

fixtures.md

File metadata and controls

355 lines (273 loc) · 11 KB

Minutest

Fixtures

What is a test fixture?

Wikipedia says:

A test fixture is something used to consistently test some item, device, or piece of software. Test fixtures can be found when testing electronics, software and physical devices.

The JUnit 4 Wiki says:

A test fixture is a fixed state of a set of objects used as a baseline for running tests. The purpose of a test fixture is to ensure that there is a well known and fixed environment in which tests are run so that results are repeatable.

So a test fixture is something to give us consistency / repeatability. In software we try where possible to create a fixture that encapsulates all the state that can affect the result of running the test, or is affected by the running of the test. That way, by creating a fresh fixture for each test, we can prevent one test run from affecting a later one.

There is a notable difference between a physical test fixture and a software test fixture. When testing a physical part it is mounted in the fixture - they are separate. When testing a software object, then the fixture will generally create that object - the subject under test will be a property of the fixture. This isn't a hard and fast rule though; in particular when testing stateless objects, stand-alone functions, or external services.

Here are some ways of using fixtures in Minutest.

No Fixture

You don't have to have a fixture. The simplest tests can just have assertions.

class NoFixtureExampleTests : JUnit5Minutests {

    fun tests() = rootContext {

        context("addition") {
            test("positive + positive") {
                assertEquals(4, 3 + 1)
            }
            test("positive + negative") {
                assertEquals(2, 3 + -1)
            }
        }
        context("subtraction") {
            test("positive - positive") {
                assertEquals(2, 3 - 1)
            }
            test("positive - negative") {
                assertEquals(4, 3 - -1)
            }
        }
    }
}

../core/src/test/kotlin/dev/minutest/examples/fixtures/NoFixtureExampleTests.kt

Subject Under Test as Fixture

It makes sense to have the subject under test as the fixture if it has the only state in the test.

class SubjectUnderTestFixtureExampleTests : JUnit5Minutests {

    fun tests() = rootContext<List<String>> {

        context("empty") {
            given {
                emptyList()
            }
            test("is empty") {
                assertTrue(it.isEmpty())
            }
            test("no head") {
                assertNull(it.firstOrNull())
            }
        }

        // Note that the context name and the fixture state agree
        context("not empty") {
            given {
                listOf("item")
            }
            test("is not empty") {
                assertFalse(it.isEmpty())
            }
            test("has head") {
                assertEquals("item", it.firstOrNull())
            }
        }
    }
}

../core/src/test/kotlin/dev/minutest/examples/fixtures/SubjectUnderTestFixtureExampleTests.kt

Even if the subject is immutable then you can inspect it in after blocks.

Arguments as Fixture

If you are testing static functions, making the arguments the fixture can be expressive.

class ArgumentsAsFixtureExampleTests : JUnit5Minutests {

    data class Arguments(val l: Int, val r: Int)

    fun tests() = rootContext<Arguments> {

        context("positive positive") {
            given {
                Arguments(l = 3, r = 1)
            }
            test("addition") {
                assertEquals(4, l + r)
            }
            test("subtraction") {
                assertEquals(2, l - r)
            }
        }

        context("positive negative") {
            given {
                Arguments(l = 3, r = -1)
            }
            test("addition") {
                assertEquals(2, l + r)
            }
            test("subtraction") {
                assertEquals(4, l - r)
            }
        }
    }
}

../core/src/test/kotlin/dev/minutest/examples/fixtures/ArgumentsAsFixtureExampleTests.kt

Again, where possible having the context name expressed in the fixture state, and vice-versa, keeps things honest.

Compound Fixture

When testing a system that mediates between other components, it makes sense to bring them all into the fixture - this gives the test isolation and repeatability that is the point of the fixture.

class ControlPanel(
    private val beep: () -> Unit,
    private val launchRocket: () -> Unit
) {
    private var keyTurned: Boolean = false

    fun turnKey() {
        keyTurned = true
    }

    fun pressButton() {
        if (keyTurned)
            launchRocket()
        else
            beep()
    }
    val warningLightOn get() = keyTurned
}

class CompoundFixtureExampleTests : JUnit5Minutests {

    // The fixture consists of all the state affected by tests
    class Fixture {
        var beeped = false
        var launched = false

        val controlPanel = ControlPanel(
            beep = { beeped = true },
            launchRocket = { launched = true }
        )
    }

    fun tests() = rootContext<Fixture> {
        given { Fixture() }

        context("key not turned") {
            test("light is off") {
                assertFalse(controlPanel.warningLightOn)
            }
            test("cannot launch when pressing button") {
                controlPanel.pressButton()
                assertTrue(beeped)
                assertFalse(launched)
            }
        }

        context("key turned") {
            beforeEach {
                controlPanel.turnKey()
            }
            test("light is on") {
                assertTrue(controlPanel.warningLightOn)
            }
            test("launches when pressing button") {
                controlPanel.pressButton()
                assertFalse(beeped)
                assertTrue(launched)
            }
        }
    }
}

../core/src/test/kotlin/dev/minutest/examples/fixtures/CompoundFixtureExampleTests.kt

Parent Fixtures

Fixtures are inherited from the parent context, and may be replaced or modified by children.

class ParentFixtureExampleTests : JUnit5Minutests {

    data class Fixture(var fruit: String)

    fun tests() = rootContext<Fixture> {
        given {
            Fixture("banana")
        }

        test("sees the context's fixture") {
            assertEquals("banana", it.fruit)
        }

        context("context inherits fixture") {
            test("sees the parent context's fixture") {
                assertEquals("banana", it.fruit)
            }
        }

        context("context replaces fixture") {
            given {
                Fixture("kumquat")
            }
            test("sees the replaced fixture") {
                assertEquals("kumquat", it.fruit)
            }
        }

        context("context modifies fixture") {
            beforeEach {
                it.fruit = "apple"
            }
            test("sees the modified fixture") {
                assertEquals("apple", it.fruit)
            }
        }
    }
}

../core/src/test/kotlin/dev/minutest/examples/fixtures/ParentFixtureExampleTests.kt

Changing Fixture Type

A context may change the type of its parent fixture.

// You can change the fixture type as you go down the context tree.
@Suppress("USELESS_IS_CHECK")
class DerivedContextExampleTests : JUnit5Minutests {

    // Fruit and FruitDrink are our 2 fixture types

    data class Fruit(val name: String)

    data class FruitDrink(val fruit: Fruit, val name: String) {
        override fun toString() = "${fruit.name} $name"
    }

    // Our root fixture type is Fruit
    fun tests() = rootContext<Fruit>("Fruit Context") {

        given {
            Fruit("banana")
        }

        test("takes Fruit") {
            assertTrue(it is Fruit)
        }

        // To change fixture type use context_
        context_<FruitDrink>("FruitDrink Context") {

            // deriveFixture specifies how to convert a Fruit to a FruitDrink
            given_ { parentFixture ->
                FruitDrink(parentFixture, "smoothie")
            }

            test("takes FruitDrink") {
                assertTrue(it is FruitDrink)
            }

            // If you don't need access to the parent fixture, this would do
            // given {
            //     FruitDrink(Fruit("kumquat"), "milkshake")
            // }
        }
    }
}

../core/src/test/kotlin/dev/minutest/examples/DerivedContextExampleTests.kt

Fixture Lifecyle

If a fixture has resources that should be disposed of, you can make it AutoCloseable and use closeableFixture.

class CloseableFixtureExampleTests : JUnit5Minutests {

    class Fixture(file: File): Closeable {

        val writer = FileWriter(file)

        override fun close() = writer.close()
    }

    fun tests() = rootContext<Fixture> {

        givenClosable { testDescriptor ->
            Fixture(File.createTempFile(testDescriptor.name, ".tmp"))
        }

        test("can write") {
            writer.write("banana")
        }

        afterEach {
            assertThrows(IOException::class.java) {
                writer.write("should be closed")
            }
        }
    }
}

../core/src/test/kotlin/dev/minutest/examples/CloseableFixtureExampleTests.kt

More Reading

My New Test Model discusses fixtures and contexts in more detail.