JUnit5 Custom Extension: ParameterResolver with Kotlin

Lets start with the question you have in your mind. What is a JUnit5 custom Extension?
A JUnit5 custom extension extends your current Unit Test with your reusable custom code. Extensions are like interceptors. JUnit provides interfaces which you can implement to execute your code. For example BeforeAllCallback
interface runs before @BeforeAll, BeforeEachCallback
runs before @BeforeEach. If you want to know more just click me. But it also provides interfaces like the one we will implement now to provide custom parameters which you can inject
in your test. The ParameterResolver interface.
But how can I write a JUnit5 custom extension?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@Target(AnnotationTarget.CLASS) @ExtendWith(IdGenerator.IdGeneratorExtension::class) annotation class IdGenerator { class IdGeneratorExtension : ParameterResolver { override fun supportsParameter(parameterContext: ParameterContext, context: ExtensionContext): Boolean { val supportedParameterTypes = listOf(UserId::class.java, Vehicle::class.java) return parameterContext.parameter.type in supportedParameterTypes } override fun resolveParameter(parameterContext: ParameterContext, context: ExtensionContext): Any { val id = generateId() return when (val type = parameterContext.parameter.type) { UserId::class.java -> UserId(id) Vehicle::class.java -> Vehicle(id) else -> throw ParameterNotSupportedException(type) } } private fun generateId(length: Int = 16): String { val chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" return (1..length).map { Random.nextInt(chars.length) }.map(chars::get).joinToString("") } } class ParameterNotSupportedException(type: Class<*>) : Throwable(“Parameter [$type] is not handled by the IdGenerator. Please add it.“) } |
Let us take a look on the IdGenerator Annotation. The annotation itself contains a class which is used to extend the IdGenerator annotation via @ExtendWith
. The Class IdGeneratorExtension implements the ParameterResolver
interface which contains two functions.
supportsParameter
: This defines if a parameter will be injected into the test function or not. This function needs to return therefore a boolean. And only if this function returnstrue
the other functionresolveParameter
will be called.
ℹ️ In my case I only return true if the parameter of the test function which I will show you in the end is one of my supported objects.
resolveParameter
: This function returns then the object which will be passed to the test function as a parameter.
ℹ️ In my case I calculate an Id with generateId()
. In the next step I check the type of the parameter of the testing function using the parameterContext
. So if my unit test adds a parameter of type UserId I will pass an UserId object.
How do I add a JUnit5 custom extension in my test?
Easier than you think 🐶
1 2 3 4 5 6 7 8 |
@IdGenerator class TestMe { @Test fun `this is a test`(vehicleId: VehicleId, userId1: UserId, userId2: UserId) { println("VehicleId: $vehicleId, UserId1: $userId1, UserId2: $userId2") } } |
Thats it. Nothing fancy 😉 Just use the annotation on class level and add as many parameters you want 🙆
The magic is done on the annotation itself because the @ExtendWith
was used while declaring the @IdGenerator annotation.
The interesting part is that for each parameter of my test-function the function resolveParameter
of the ParameterResolver
is called. Therefore I can add as much parameters as I want and every parameter will have its value (when I handled the type of the parameter in my custom extension function of course 😉 )
In my test it is really important that each id is unique. And to guarantee this we somehow need to check that the generated id was not generated before. Although the chance that this can happen is really low 😅
I need to save a state so how can I use the store in Jupiter Extensions?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
@Target(AnnotationTarget.CLASS) @ExtendWith(IdGenerator.IdGeneratorExtension::class) annotation class IdGenerator { class IdGeneratorExtension : BeforeAllCallback, ParameterResolver { private companion object { // 1) Create the JUnit Store where all generatedIds will be saved val STORE = ExtensionContext.Namespace.create(this::class) const val IDS = "ids" } // 2) Implement the BeforeAllCallback interface and put an mutable empty Set in the JUnit Store beforeAll tests were executed override fun beforeAll(context: ExtensionContext) { context.getStore(STORE).put(IDS, mutableSetOf<String>()) } override fun supportsParameter(parameterContext: ParameterContext, context: ExtensionContext): Boolean { val supportedParameterTypes = listOf(UserId::class.java, VehicleId::class.java) return parameterContext.parameter.type in supportedParameterTypes } override fun resolveParameter(parameterContext: ParameterContext, context: ExtensionContext): Any { // 3) Return all generatedIds from the JUnit Store val generatedIds = context.getStore(STORE).get(IDS) as MutableSet<String> val id = generateId() // 4) If the id was already generated the function is recursively called again until the id is unique if (id in generatedIds) { resolveParameter(parameterContext, context) } return when (val type = parameterContext.parameter.type) { UserId::class.java -> UserId(id) VehicleId::class.java -> VehicleId("v$id") else -> throw ParameterNotSupportedException(type) } } private fun generateId(length: Int = 16): String { val chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" return (1..length).map { Random.nextInt(chars.length) }.map(chars::get).joinToString("") } } class ParameterNotSupportedException(type: Class<*>) : Throwable("Parameter [$type] is not handled by the IdGenerator. Please add it.") } |
What has changed?
- I create now a
STORE
in the the private companion object which creates a Jupiter store for the test instance. The store is like a key value map which is persistent while your test instance is present. - I implemented a new interface called
BeforeAllCallback
which forces you to implement thebeforeAll
function. This is executed before all Tests of your test class (also before each @Nested inner classes). Inside that function we take advantage of the store.
We get the store via theSTORE
variable which was created in 1). After we got the store we set a new entry with the keyIDS
. As a value we create an emptymutableSetOf<String>()
. - What else? Now we need to adjust the
resolveParameter
function. There we now get our createdmutableSetOf<String>()
by accessing the keyIDS
. - And here we just check if the generated id from the line before is already present. If yes, call the whole function again to generate a new id. If no, add the id to the mutableSet and put the set into the store.
Cool! Now we prevent duplicate same ids. Easy right? 😎
Aktuelle Beiträge






Artikel kommentieren