3 min read

Kotlin Enum Classes as Test Parameters

Kotlin Enum Classes as Test Parameters

Writing parameterized test cases enables developers to improve code robustness without writing multiple flavors of the same test or burying the majority of testing logic in helper methods away from the test itself. Succinctly put, test parameters provide more coverage with less code. Recently, I found myself in need of a convention to share parameters across multiple unit tests. In some cases, I only need a subset of the fixtures to verify specific behavior.

As primarily an Android-Kotlin developer, I use TestParameterInjector by Google when parameterizing a test suite, and as the post title implies, I prefer using enum classes as test parameters. I favor enums because, by default, the TestParameterInjector::class runner will inject the enum directly into your test with nothing more than the @TestParameter annotation.

enum class Fruit {
	Apple, Orange, Pear
}

@RunWith(TestParameterInjector::class)
class ExampleTest {
    @Test
    fun `test with all fruits`(@TestParameter fruit: Fruit) {}
}
Injecting an enum class into a test

The above example is simple enough, but assume we have the following app we would like to test.

data class Person(val name: String, val age: Int)

class Greeter {
    fun greet(person: Person) = "Hello ${person.name}!"
}
A greeter example

A reasonable GreeterTest may take the following form.

class GreeterTest {
    private val greeter = Greeter()
    
    @Test
    fun `the greeter greets`() {
        val person = Person(name = "Alice", age = 30)
        assertThat(greeter.greet(person))
        	.isEqualTo("Hello ${person.name}!")
    }
}
A greeter test example

Now, assume that Greeter behaves differently for different folks. In this case, TestParameterInjector provides a TestParameterValuesProvider interface to provide dynamic parameters. One approach the documentation describes is to use Junit assume(...) to skip parameters. This approach works, but in my opinion, it does not read as clearly as using classes that state exactly what parameters the test needs.

Instead, I prefer to define a generic enum test fixture class. This approach yields the following benefits.

  1. Adding more enum values can increase test coverage
  2. When no filtering or parameter transformation is needed, only the @TestParameter annotation is required when used in a test
  3. Filtering or transforming the parameters is possible with the Enum.entries (since Kotlin 1.9) or Enum.values()
  4. Accessing specific enum entries can be done by name using @TestParameter("EnumEntryName")
  5. Building off the previous point, test authors can name the input as the strongly typed enum entry itself

An enum as a test fixture for the greeter example may look like the following.

enum class GreeterTestFixture(val person: Person) {
    Alice(person = Person(name = "Alice", age = 30)),
    Bob(person = Person(name = "Bob", age = 31))
}

abstract class BaseTestParameterProvider(
    private val parameters: () -> List<Any>
) : TestParameterValuesProvider {
    override fun provideValues(): List<Any> = parameters()
}

class PeopleOverThirty : BaseTestParameterProvider({
    GreeterTestFixture.values().filter {
        it.person.age > 30
    }
})

class PeopleWithNamesStartingInA : BaseTestParameterProvider({
    GreeterTestFixture.values().filter {
        it.person.name.lowercase().startsWith("a")
    }
})
An example of an enum as a test fixture—if anyone can improve the BaseTestParameterProvider API, please reach out 🙏🏼

And now, putting it all together in a test class produces the following tests.

@RunWith(TestParameterInjector::class)
class GreeterParameterizedTest {
    private val greeter = Greeter()

    @Test
    fun `the greeter greets`(
        @TestParameter greeterTestFixture: GreeterTestFixture
    ) {}

    @Test
    fun `the greeter greets people over thirty`(
        @TestParameter(valuesProvider = PeopleOverThirty::class)
        greeterTestFixture: GreeterTestFixture
    ) {}

    @Test
    fun `the greeter greets people whose name starts with a`(
        @TestParameter(valuesProvider = PeopleWithNamesStartingInA::class)
        greeterTestFixture: GreeterTestFixture
    ) {}
    
    @Test
    fun `the greeter greets alice`(
        @TestParameter("Alice")
        greeterTestFixture: GreeterTestFixture
    ) {}
}
A test suite that demonstrates Enum classes as test parameters

It is worth noting that this pattern—or even parameterizing tests in general—does come with tradeoffs. Test parameterizing with enums makes access to test inputs easier. As a result, you or your team may find yourself using test inputs in tests where the permutations do not improve the validation. When I was in college, I remember software testing being described as "make a graph and cover it" with your tests. As a general rule of thumb, parameterize your tests with inputs that only increase your test coverage.

Secondly, I find that test parameters, in an effort to reduce test copying, invite the definition of expectations in the test fixtures themselves—something akin to enum class GreeterTestFixture(val person: Person, expectedGreeting: String). While I occasionally use this pattern, I try to limit the expectations because encoding all the test inputs and expectations into the fixtures may come at a cost to maintainability and readability as new developers ramping up to the component may examine the tests only to find that most of the business logic and expectations are found in the test data rather than the tests themselves.