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.
The above example is simple enough, but assume we have the following app we would like to test.
A reasonable GreeterTest
may take the following form.
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.
- Adding more enum values can increase test coverage
- When no filtering or parameter transformation is needed, only the
@TestParameter
annotation is required when used in a test - Filtering or transforming the parameters is possible with the
Enum.entries
(since Kotlin 1.9) orEnum.values()
- Accessing specific enum entries can be done by name using
@TestParameter("EnumEntryName")
- 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.
And now, putting it all together in a test class produces the following tests.
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.