Writing Parameterized Tests With Kotlin and JUnit 5, Part One

After we have finished this webinar, we

  • Can get the required dependencies with Maven and Gradle.
  • Can write parameterized tests with JUnit 5 and Kotlin.
  • Know how we can customize the name of each test function invocation.
  • Understand how we can use different argument sources.

Get the source code from Github

Getting the Required Dependencies

Before we can write parameterized tests with JUnit 5, we have to add the junit-jupiter-params dependency to our build.

If we are using Maven, we can add this dependency to our Maven build by adding the following snippet to the dependencies section of our POM file:

1<dependency>
2    <groupId>org.junit.jupiter</groupId>
3    <artifactId>junit-jupiter-params</artifactId>
4    <version>5.4.2</version>
5    <scope>test</scope>
6</dependency>

If we are using Gradle, we have to add the junit-jupiter-params dependency to the testImplementation dependency configuration. We can do this by adding the following snippet to our build.gradle file:

1testImplementation(
2		'org.junit.jupiter:junit-jupiter-params:5.4.2'
3)
The testImplementation dependency configuration is a new dependency configuration that replaces the testCompile dependency configuration. If you want to get more information about this dependency configuration, you should take a look at this StackOverflow answer .

Let’s move on and write our first parameterized test with JUnit 5.

Writing Our First Parameterized Test With JUnit 5

We can write a simple parameterized test with JUnit 5 by following these steps:

  1. Add a new test function to our test class and specify the function parameters that should be passed to our test function.
  2. Annotate the test function with the @DisplayName annotation and specify the display name.
  3. Annotate the test function with the @ParameterizedTest annotation. This annotation identifies parameterized test functions.
  4. Provide the function parameters that are passed to our test function. Because our test function takes one String object as a function parameter, we can provide the function parameters by annotating our test function with the @ValueSource annotation.

After we have written our first parameterized test, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.ValueSource
 6
 7@Tag("unitTest")
 8@DisplayName("@ValueSource example")
 9class ValueSourceExampleTest {
10
11    @DisplayName("Use the parameters provided by the @ValueSource annotation")
12    @ParameterizedTest
13    @ValueSource(strings = ["Hello", "World"])
14    fun shouldPassNotNullMessageAsFunctionParameter(message: String) {
15        assertThat(message).isNotNull()
16    }
17}

The @ValueSource annotation allows us to specify the function parameters by using an array that can contain literal values. Also, we can use this annotation only if our test function takes only one function parameter.

Additional Reading:

When we run our parameterized test, we should see an output that looks as follows:

1@ValueSource example
2|_	Use the parameters provided by the @ValueSource annotation
3	|_	[1] Hello
4	|_	[2] World

Even though this output looks quite clean, sometimes we want to provide our own display name for each function invocation. Let’s find out how we can do it.

Customizing the Display Name of Each Function Invocation

We can customize the display name of each function invocation by setting the value of the @ParameterizedTest annotation’s name attribute. This attribute supports the following placeholders:

  • {index}: The index of the current invocation. Note that the index of the first invocation is 1.
  • {arguments}: A comma separated list that contains all function parameters.
  • {i}: The index of the actual function parameter. Note that the index of the first function parameter is 0.

Let’s provide a custom display name to our test function. This display name must display the index of the current invocation and the used function parameter.

After we have customized the display name of our test function, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.ValueSource
 6
 7@Tag("unitTest")
 8@DisplayName("@ValueSource example")
 9class ValueSourceExampleTest {
10
11    @DisplayName("Use the parameters provided by the @ValueSource annotation")
12    @ParameterizedTest(name = "{index} => message=''{0}''")
13    @ValueSource(strings = ["Hello", "World"])
14    fun shouldPassNotNullMessageAsFunctionParameter(message: String) {
15        assertThat(message).isNotNull()
16    }
17}

When we run our parameterized test, we should see an output that looks as follows:

1@ValueSource example
2|_	Use the parameters provided by the @ValueSource annotation
3	|_	1 => message='Hello'
4	|_	2 => message='World'

When we wrote our first parameterized test, we used one argument source (@ValueSource) that’s supported by JUnit 5. Next, we will find out how we can use the other supported argument sources.

Using the Other Argument Sources

JUnit 5 has a quite versatile support for different argument sources. Next, we will take look at eight different argument sources. Let’s start by taking a look at the @NullSource annotation.

Using the @NullSource Annotation

If we want to pass a single null parameter to our test function, we have to annotate our test function with the @NullSource annotation. After we have done this, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.NullSource
 6
 7@Tag("unitTest")
 8@DisplayName("@NullSource and @EmptySource example")
 9class NullAndEmptySourceExampleTest {
10
11    @DisplayName("Should pass a single null value as a function parameter")
12    @ParameterizedTest(name = "{index} => text={0}")
13    @NullSource
14    fun shouldPassSingleNullValueAsFunctionParameter(text: String?) {
15        assertThat(text).isNull()
16    }
17}

Next, we will find out how we can provide a simple empty parameter to our test function.

Using the @EmptySource Annotation

If we want to pass a single empty parameter to our test function, we have to annotate our test function with the @EmptySource annotation. After we have done this, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.EmptySource
 6
 7@Tag("unitTest")
 8@DisplayName("@NullSource and @EmptySource example")
 9class NullAndEmptySourceExampleTest {
10
11    @DisplayName("Should pass a single empty string as a function parameter")
12    @ParameterizedTest(name = "{index} => text={0}")
13    @EmptySource
14    fun shouldPassSingleEmptyStringAsFunctionParameter(text: String) {
15        assertThat(text).isEmpty()
16    }
17}

The @EmptySource annotation supports the following types: java.lang.String, java.util.List, java.util.Set, java.util.Map, primitive arrays (e.g. int[]), and object arrays (e.g. Integer[]).

Additional Reading:

Let’s move on and find out how we can pass a single null parameter and a single empty parameter to our test function.

Using the @NullAndEmptySource Annotation

The @NullAndEmptySource annotation combines the functionality of the @NullSource and @EmptySource annotations. In other words, if we want to pass a single null parameter and a single empty parameter to our test function, we have to annotate it with @NullAndEmptySource annotation.

After we have done this, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.NullAndEmptySource
 6
 7@Tag("unitTest")
 8@DisplayName("@NullSource and @EmptySource example")
 9class NullAndEmptySourceExampleTest {
10
11    @DisplayName("Should pass null and empty string as function parameters")
12    @ParameterizedTest(name = "{index} => text={0}")
13    @NullAndEmptySource
14    fun shouldPassNullAndEmptyStringAsFunctionParameters(text: String?) {
15        assertThat(text).isNullOrEmpty()
16    }
17}

Next, we will find out how we can combine null and empty parameters with parameters provided by the @ValueSource annotation.

Combining Null and Empty Parameters With the @ValueSource Annotation

We can combine the @NullSource, @EmptySource, and @NullAndEmptySource annotations with the @ValueSource annotation annotation. Let’s write a parameterized test and pass a single null value, a single empty String, and different whitespace strings as function parameters.

After we have written our parameterized test, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.NullAndEmptySource
 6import org.junit.jupiter.params.provider.ValueSource
 7
 8@Tag("unitTest")
 9@DisplayName("@NullSource and @EmptySource example")
10class NullAndEmptySourceExampleTest {
11
12    @DisplayName("Should all provided values as function parameters")
13    @ParameterizedTest(name = "{index} => text={0}")
14    @NullAndEmptySource
15    @ValueSource(strings = ["", " ", "\t", "\n"])
16    fun shouldPassAllProvidedValuesAsFunctionParameters(text: String?) {
17        assertThat(text?.trim()).isNullOrEmpty()
18    }
19}

Let’s move on and find out how we can pass enum values to our parameterized test.

Using the @EnumSource Annotation

The @EnumSource annotation allows us to specify the enum values which are passed to our parameterized test. Let’s assume that we have to write a parameterized test that takes the values of the Pet enum as function parameters. The source code of the Pet enum looks as follows:

1enum class Pet {
2    CAT,
3    DOG
4}

If we want to pass all enum values to our test function we have to annotate our test function with the @EnumSource annotation and specify the enum whose values are passed to our test function. After we have done this, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.EnumSource
 6
 7@Tag("unitTest")
 8@DisplayName("@EnumSource example")
 9class EnumSourceExampleTest {
10
11    @DisplayName("Should pass all enum values as function parameters")
12    @ParameterizedTest(name = "{index} => pet=''{0}''")
13    @EnumSource(Pet::class)
14    fun shouldPassNotNullEnumValuesAsFunctionParameters(pet: Pet) {
15        assertThat(pet).isNotNull()
16    }
17}

If we want to specify the enum values that are passed to our test function, we have to specify the enum values by setting the value of the @EnumSource annotation’s names attribute. Let’s ensure that the value: Pet.CAT is passed to our test function.

After we have specified the used enum value, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.EnumSource
 6
 7@Tag("unitTest")
 8@DisplayName("@EnumSource example")
 9class EnumSourceExampleTest {
10
11    @DisplayName("Should pass Pet.CAT as a function parameter")
12    @ParameterizedTest(name = "{index} => pet=''{0}''")
13    @EnumSource(value = Pet::class, names = ["CAT"])
14    fun shouldPassSpecifiedValueAsFunctionParameter(pet: Pet) {
15        assertThat(pet).isEqualTo(Pet.CAT)
16    }
17}

Next, we will find out how we can pass multiple parameters to our test function.

Using the @CsvSource Annotation

If our test data is used by only one parameterized test, we can configure our test data by using the @CsvSource annotation. When we add this annotation to a test function, we have to configure the test data by using an array of String objects. Each string contains a comma separated list that defines the function parameters of our test function.

Let’s configure the function parameters which are passed to the sum() function. After we have done this, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.CsvSource
 6
 7@Tag("unitTest")
 8@DisplayName("@CsvSource example")
 9internal class CsvSourceExampleTest {
10
11    @DisplayName("Use the parameters provided by the @CsvSource annotation")
12    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
13    @CsvSource("1, 1, 2", "2, 3, 5")
14    fun sum(a: Int, b: Int, sum: Int) {
15        assertThat(a + b).isEqualByComparingTo(sum)
16    }
17}

Even though this looks quite clean, there are two situations when using the @CsvSource annotation doesn’t make any sense:

  • We have so much test data that we cannot add it to our test class because our test class would become unreadable.
  • The same test data is used by multiple test functions.

Let’s move on and find out how we solve these two problems by loading our test data from a CSV file.

Using the @CsvFileSource Annotation

We can load our test data from a CSV file by following these steps:

First, we have to create a CSV file that contains our test data and put this file to the classpath. When we add our test data to the created CSV file, we have to follow these rules:

  • One line must contain all function parameters of one invocation.
  • The values found from each line must use the same order as the function parameters of our test function.

The test-data.csv file configures the test data that is passed to our test function. This file is found from the src/test/resources directory, and its content looks as follows:

11,1,2
22,3,5
33,5,8

Second, we have to annotate our test function with the @CsvFileSource annotation and configure the location of our CSV file. After we have done this, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.CsvFileSource
 6import org.junit.jupiter.params.provider.CsvSource
 7
 8@Tag("unitTest")
 9@DisplayName("@CsvFileSource example")
10internal class CsvFileSourceExampleTest {
11
12    @DisplayName("Use the parameters provided by the test-data.csv file")
13    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
14    @CsvFileSource(resources = ["/test-data.csv"])
15    fun sum(a: Int, b: Int, sum: Int) {
16        assertThat(a + b).isEqualByComparingTo(sum)
17    }
18}

Next, we will learn to provide our test data by using a factory function.

Using the @MethodSource Annotation

If we we want to provide our test data by using a factory function, we have to follow these steps:

  1. Add a static function to our test class. This function returns a Stream of Arguments objects and it doesn’t take any function parameters.
  2. Implement this function by returning our test data.
  3. Annotate our test function with the @MethodSource annotation and configure the name of the function that return our test data.

After we have implemented our argument source function, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.params.ParameterizedTest
 5import org.junit.jupiter.params.provider.Arguments
 6import org.junit.jupiter.params.provider.MethodSource
 7import java.util.stream.Stream
 8
 9@Tag("unitTest")
10@DisplayName("@MethodSource example")
11class MethodSourceExampleTest {
12
13    companion object {
14
15        @JvmStatic
16        private fun sumProvider(): Stream<Arguments> {
17            return Stream.of(
18                    Arguments.of(1, 1, 2),
19                    Arguments.of(2, 3, 5)
20            )
21        }
22    }
23
24    @DisplayName("Use the parameters provided by the sumProvider() function")
25    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
26    @MethodSource("sumProvider")
27    fun sum(a: Int, b: Int, sum: Int) {
28        assertThat(a + b).isEqualByComparingTo(sum)
29    }
30}

There are five things I want to point out:

  • The argument source function must return a Stream, Iterable, Iterator, or an array of arguments.
  • The argument source function can also return streams that contain primitive types.
  • The returned arguments must use the same order as the function parameters of our test function.
  • The argument source function must not take any function parameters.
  • If we use the default lifecycle configuration, the argument source function must be static.

Additional Reading:

Let’s move on and find out, how we can implement a custom ArgumentsProvider class.

Using the @ArgumentSource Annotation

If we need maximum flexibility, we can use a custom ArgumentsProvider by following these steps:

First, we have write a custom ArgumentsProvider class by following these steps:

  1. Create a class that implements the ArgumentsProvider interface.
  2. Implement the provideArguments() function by returning our test data. Remember that the returned test data must use the same order as the function parameters of our test function.

After we have created a custom ArgumentsProvider class, the source code of our test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.api.extension.ExtensionContext
 5import org.junit.jupiter.params.ParameterizedTest
 6import org.junit.jupiter.params.provider.Arguments
 7import org.junit.jupiter.params.provider.ArgumentsProvider
 8import java.util.stream.Stream
 9
10@Tag("unitTest")
11@DisplayName("@ArgumentsSource example")
12class ArgumentsSourceExampleTest {
13
14    @DisplayName("Should use the provided function parameters")
15    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
16    fun sum(a: Int, b: Int, sum: Int) {
17        assertThat(a + b).isEqualByComparingTo(sum)
18    }
19
20    internal class CustomArgumentProvider : ArgumentsProvider {
21
22        @Throws(Exception::class)
23        override fun provideArguments(context: ExtensionContext): Stream<out Arguments> {
24            return Stream.of(
25                    Arguments.of(1, 1, 2),
26                    Arguments.of(2, 3, 5)
27            )
28        }
29    }
30}
By the way, If our ArgumentsProvider class is used by multiple test classes, we don’t want to use an inner class. I used an inner class here because this is just an example.

Second, we have to configure the used ArgumentsProvider by annotating our test function with the @ArgumentsSource annotation. After we have done this, the source code of test class looks as follows:

 1import org.assertj.core.api.Assertions.assertThat
 2import org.junit.jupiter.api.DisplayName
 3import org.junit.jupiter.api.Tag
 4import org.junit.jupiter.api.extension.ExtensionContext
 5import org.junit.jupiter.params.ParameterizedTest
 6import org.junit.jupiter.params.provider.Arguments
 7import org.junit.jupiter.params.provider.ArgumentsProvider
 8import org.junit.jupiter.params.provider.ArgumentsSource
 9import java.util.stream.Stream
10
11@Tag("unitTest")
12@DisplayName("@ArgumentsSource example")
13class ArgumentsSourceExampleTest {
14
15    @DisplayName("Should use the provided function parameters")
16    @ParameterizedTest(name = "{index} => a={0}, b={1}, sum={2}")
17    @ArgumentsSource(CustomArgumentProvider::class)
18    fun sum(a: Int, b: Int, sum: Int) {
19        assertThat(a + b).isEqualByComparingTo(sum)
20    }
21
22    internal class CustomArgumentProvider : ArgumentsProvider {
23
24        @Throws(Exception::class)
25        override fun provideArguments(context: ExtensionContext): Stream<out Arguments> {
26            return Stream.of(
27                    Arguments.of(1, 1, 2),
28                    Arguments.of(2, 3, 5)
29            )
30        }
31    }
32}

We can now get the required dependencies and write parameterized tests with JUnit 5 and Kotlin. Let’s summarize what we learned from this webinar.

Summary

This webinar has taught us four things:

  • Before we can write parameterized tests with JUnit 5 and Kotlin, we have to declare the junit-jupiter-params dependency in our build script.
  • We have to annotate our parameterized test function with the @ParameterizedTest annotation.
  • We can customize the display name of each method invocation by setting the value of the @ParameterizedTest annotation’s name attribute.
  • When we configure our test data, our test data must use the same order as the function parameters of our test function.

Get the source code from Github