Using JUnit 5 Parameterized Tests, Argument Sources and Converters

August 19th, 2017 by

With JUnit 5 the possibilities to write parameterized tests have changed and improved a lot.

The following short overview covers all new types of possible parameter sources for JUnit 5 tests as well as the new conversion API for test arguments.

In addition we’re showing how parameterized tests were written in JUnit 4.

Running JUnit5 in IntelliJ

Running JUnit5 in IntelliJ

 

About

We will be covering all available types of parameter sources in the following sections – all that you need as a prerequisite is Java ™, Maven and a few minutes of your time.

How parameterized tests are written with JUnit 4 is explained in the following article of mine: “New features in JUnit 4.11“.

Dependencies

Since JUnit 5 has chosen a modular approach to structure its functionality, we need to add the following dependencies to our project’s pom.xml (using Maven):

  • junit-jupiter-engine: Public API needed for writing basic tests.
  • junit-jupiter-params: Dependencies to write parameterized tests.
  • junit-platform-launcher: Needed to launch our tests in our IDE of choice like IntelliJ/Eclipse…

In addition, we’re adding the dependency junit-platform-surefire-provider to the Surefire plugin so that we’re able to run all tests with Maven in the command-line.

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-params</artifactId>
        <version>${junit.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-launcher</artifactId>
        <version>1.0.0-M5</version>
    </dependency>
</dependencies>
 
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.19</version>
            <dependencies>
                <dependency>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>junit-platform-surefire-provider</artifactId>
                    <version>1.0.0-M5</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

Parameter Sources

Here is a list of the available sources that may be used to feed our tests with parameters…

Value Source

A value source allows you to directly specify the parameters as annotation attribute.

Currently you may use the following types here:

  • String values: @ValueSource(strings = {“foo”, “bar”, “baz”})
  • Double values: @ValueSource(doubles = {1.5D, 2.2D, 3.0D})
  • Long values: @ValueSource(longs = {2L, 4L, 8L})
  • Integer values: @ValueSource(ints = {2, 4, 8})

Here are some concrete tests using value-source:

package com.hascode.tutorial;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
 
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
 
public class ValueSourcesExampleTest {
 
  @ParameterizedTest
  @ValueSource(ints = {2, 4, 8})
  void testNumberShouldBeEven(int num) {
    assertEquals(0, num % 2);
  }
 
  @ParameterizedTest
  @ValueSource(strings = {"Radar", "Rotor", "Tenet", "Madam", "Racecar"})
  void testStringShouldBePalindrome(String word) {
    assertEquals(isPalindrome(word), true);
  }
 
  @ParameterizedTest
  @ValueSource(doubles = {2.D, 4.D, 8.D})
  void testDoubleNumberBeEven(double num) {
    assertEquals(0, num % 2);
  }
 
  boolean isPalindrome(String word) {
    return word.toLowerCase().equals(new StringBuffer(word.toLowerCase()).reverse().toString());
  }
}

We may now run these tests like this:

$ mvn test -Dtest=ValueSourcesExampleTest
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.hascode.tutorial.ValueSourcesExampleTest
Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.087 sec - in com.hascode.tutorial.ValueSourcesExampleTest
 
Results :
 
Tests run: 11, Failures: 0, Errors: 0, Skipped: 0
Parameterized test using value sources.

Parameterized test using value sources.

Enum Source

This parameter source allows us to pass in the values of a given enum and additionally restrict which of its values are passed in as parameters using restricting lists or by matching the values to a regular expression.

Let’s take a further look at these concrete examples:

package com.hascode.tutorial;
 
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
 
import java.util.EnumSet;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.EnumSource.Mode;
 
public class EnumSourcesExampleTest {
 
  @ParameterizedTest(name = "[{index}] TimeUnit: {arguments}")
  @EnumSource(TimeUnit.class)
  void testTimeUnitMinimumNanos(TimeUnit unit) {
    assertTrue(unit.toMillis(2000000L) > 1);
  }
 
  @ParameterizedTest
  @EnumSource(value = TimeUnit.class, names = {"SECONDS", "MINUTES"})
  void testTimeUnitJustSecondsAndMinutes(TimeUnit unit) {
    assertTrue(EnumSet.of(TimeUnit.SECONDS, TimeUnit.MINUTES).contains(unit));
    assertFalse(EnumSet
        .of(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS,
            TimeUnit.MICROSECONDS).contains(unit));
  }
 
  @ParameterizedTest
  @EnumSource(value = TimeUnit.class, mode = Mode.EXCLUDE, names = {"SECONDS", "MINUTES"})
  void testTimeUnitExcludingSecondsAndMinutes(TimeUnit unit) {
    assertFalse(EnumSet.of(TimeUnit.SECONDS, TimeUnit.MINUTES).contains(unit));
    assertTrue(EnumSet
        .of(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS,
            TimeUnit.MICROSECONDS).contains(unit));
  }
 
  @ParameterizedTest
  @EnumSource(value = TimeUnit.class, mode = Mode.MATCH_ALL, names = ".*SECONDS")
  void testTimeUnitIncludingAllTypesOfSecond(TimeUnit unit) {
    assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS, TimeUnit.MINUTES).contains(unit));
    assertTrue(EnumSet
        .of(TimeUnit.SECONDS, TimeUnit.MILLISECONDS, TimeUnit.NANOSECONDS,
            TimeUnit.MICROSECONDS).contains(unit));
  }
 
}

Again we may run our tests with Maven in the console:

$ mvn test -Dtest=EnumSourcesExampleTest
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.hascode.tutorial.EnumSourcesExampleTest
Tests run: 18, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.104 sec - in com.hascode.tutorial.EnumSourcesExampleTest
 
Results :
 
Tests run: 18, Failures: 0, Errors: 0, Skipped: 0
Parameterized test using enum source

Parameterized test using enum source

Method Source

This parameter source allows us to reference a method that provides the input parameters. The referenced method must return either a Stream, an Iterator or an Iterable.

package com.hascode.tutorial;
 
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
 
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
 
public class MethodSourceExampleTest {
  @ParameterizedTest
  @MethodSource("stringGenerator")
  void shouldNotBeNullString(String arg){
    assertNotNull(arg);
  }
 
  @ParameterizedTest
  @MethodSource("intGenerator")
  void shouldBeNumberWithinRange(int arg){
    assertAll(
        () -> assertTrue(arg > 0),
        () -> assertTrue(arg <= 10)
    );
  }
 
  @ParameterizedTest(name = "[{index}] user with id: {0} and name: {1}")
  @MethodSource("userGenerator")
  void shouldUserWithIdAndName(long id, String name){
        assertNotNull(id);
        assertNotNull(name);
  }
 
  static Stream<String> stringGenerator(){
    return Stream.of("hello", "world", "let's", "test");
  }
 
  static IntStream intGenerator() {
    return IntStream.range(1,10);
  }
 
  static Stream<Arguments> userGenerator(){
    return Stream.of(Arguments.of(1L, "Sally"), Arguments.of(2L, "Terry"), Arguments.of(3L, "Fred"));
  }
}

We’re running our tests in the console with Maven again:

$ mvn test -Dtest=MethodSourceExampleTest
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.hascode.tutorial.MethodSourceExampleTest
Tests run: 16, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.104 sec - in com.hascode.tutorial.MethodSourceExampleTest
 
Results :
 
Tests run: 16, Failures: 0, Errors: 0, Skipped: 0
Parameterized test using method source

Parameterized test using method source

Argument Source

This parameter source allows us to reference a Java class that provides the parameters for the test run.

The providing class must implement the interface ArgumentsProvider, so we need to implement just one method that returns a stream of arguments.

For a better understanding, we’ll have a look at the following example:

package com.hascode.tutorial;
 
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
 
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
 
public class ArgumentsSourceExampleTest {
 
  @ParameterizedTest
  @ArgumentsSource(CustomArgumentsGenerator.class)
  void testGeneratedArguments(double number) throws Exception {
    assertFalse(number == 0.D);
    assertTrue(number > 0);
    assertTrue(number < 1);
  }
 
  static class CustomArgumentsGenerator implements ArgumentsProvider {
 
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
      return Stream.of(Math.random(), Math.random(), Math.random(), Math.random(), Math.random())
          .map(Arguments::of);
    }
  }
}

Running the tests again in the command-line:

$ mvn test -Dtest=ArgumentsSourceExampleTest
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.hascode.tutorial.ArgumentsSourceExampleTest
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.071 sec - in com.hascode.tutorial.ArgumentsSourceExampleTest
 
Results :
 
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
Parameterized tests using argument sources

Parameterized tests using argument sources

CSV Source

This parameter source allows us to specify arguments using strings with comma-separated-values as annotation parameters.

Let’s take a look at a concrete example test:

package com.hascode.tutorial;
 
import static org.junit.jupiter.api.Assertions.assertTrue;
 
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
 
public class CsvSourceExampleTest {
 
  Map<Long, String> idToUsername = new HashMap<>();
 
  {
    idToUsername.put(1L, "Selma");
    idToUsername.put(2L, "Lisa");
    idToUsername.put(3L, "Tim");
  }
 
  @ParameterizedTest
  @CsvSource({"1,Selma", "2,Lisa", "3,Tim"})
  void testUsersFromCsv(long id, String name) {
    assertTrue(idToUsername.containsKey(id));
    assertTrue(idToUsername.get(id).equals(name));
  }
}

Running the test should produce a similar output to this one:

$ mvn test -Dtest=CsvSourceExampleTest
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.hascode.tutorial.CsvSourceExampleTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.097 sec - in com.hascode.tutorial.CsvSourceExampleTest
 
Results :
 
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
Parameterized test using CSV source

Parameterized test using CSV source

CSV File Source

In addition to the CSV source, we may also specify a file that contains our test arguments as comma-separated-values.

This CSV file named users.csv is used as parameter source for the following test:

1,Selma
2,Lisa
3,Tim

And this is our test reading its arguments from the CSV file:

package com.hascode.tutorial;
 
import static org.junit.jupiter.api.Assertions.assertTrue;
 
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.junit.jupiter.params.provider.CsvSource;
 
public class CsvFileSourceExampleTest {
 
  Map<Long, String> idToUsername = new HashMap<>();
 
  {
    idToUsername.put(1L, "Selma");
    idToUsername.put(2L, "Lisa");
    idToUsername.put(3L, "Tim");
  }
 
  @ParameterizedTest
  @CsvFileSource(resources = "/users.csv")
  void testUsersFromCsv(long id, String name) {
    assertTrue(idToUsername.containsKey(id));
    assertTrue(idToUsername.get(id).equals(name));
  }
}

Running the test should produce a similar result:

$ mvn test -Dtest=CsvFileSourceExampleTest
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.hascode.tutorial.CsvFileSourceExampleTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.102 sec - in com.hascode.tutorial.CsvFileSourceExampleTest
 
Results :
 
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
Parameterized test using CSV file source

Parameterized test using CSV file source

Argument Conversion

JUnit allows us to convert arguments to the target format we need in our tests.

There are two possible conversion types:

Implicit Conversion

JUnit offers multiple built-in type converters, especially to convert between strings and the common value types as this is needed for the CSV sources.

The following target types for a by-string conversion are available (primitives as their wrapper types):

  • Boolean
  • Byte
  • Character
  • Short
  • Integer
  • Long
  • Float
  • Double
  • Enum subclass
  • Instant
  • LocalDate
  • LocalDateTime
  • LocalTime
  • OffsetTime
  • OffsetDateTime
  • Year
  • YearMonth
  • ZonedDateTime

For more detailed information, please refer to the JUnit User Manual here.

Explicit Conversion

If we want to control the conversion or we need to convert to a type that is not in the list above, we may use the annotation @ConvertWith(MyConverter.class) for our argument and let our converter class implement SimpleArgumentConverter.

The following example shows a converter in action that converts between a string and an UUID.

package com.hascode.tutorial;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
 
import java.time.LocalDate;
import java.time.Month;
import java.util.UUID;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.converter.ConvertWith;
import org.junit.jupiter.params.converter.SimpleArgumentConverter;
import org.junit.jupiter.params.provider.ValueSource;
 
public class ArgumentsConversionExampleTest {
 
  @ParameterizedTest
  @ValueSource(strings = "2017-07-11")
  void testImplicitArgumentConversion(LocalDate date) throws Exception {
    assertTrue(date.getYear() == 2017);
    assertTrue(date.getMonth().equals(Month.JULY));
    assertTrue(date.getDayOfMonth() == 11);
  }
 
  @ParameterizedTest
  @ValueSource(strings = "B4627B3B-ACC4-44F6-A2EB-FCC94DAB79A5")
  void testImplicitArgumentConversion(@ConvertWith(ToUUIDArgumentConverter.class) UUID uuid)
      throws Exception {
    assertNotNull(uuid);
    assertTrue(uuid.getLeastSignificantBits() == -6706989278516512347L);
  }
 
  static class ToUUIDArgumentConverter extends SimpleArgumentConverter {
 
    @Override
    protected Object convert(Object source, Class<?> targetType) {
      assertEquals(UUID.class, targetType, "may only convert to UUID");
      return UUID.fromString(String.valueOf(source));
    }
  }
 
}

Running our tests should look similar to this output:

$ mvn test -Dtest=ArgumentsConversionExampleTest
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.hascode.tutorial.ArgumentsConversionExampleTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.083 sec - in com.hascode.tutorial.ArgumentsConversionExampleTest
 
Results :
 
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
Argument conversion in a parameterized test

Argument conversion in a parameterized test

Tutorial Sources

Please feel free to download the tutorial sources from my Bitbucket repository, fork it there or clone it using Git:

git clone https://bitbucket.org/hascode/junit5-params-sample.git

Resources

Troubleshooting

  • java.lang.NoSuchMethodError: org.junit.platform.commons.util.ReflectionUtils.getDefaultClassLoader(.. when running in IDE like IntelliJ” Add the following dependency to your project’s pom.xml:
    <dependency>
        <groupId>org.junit.platform</groupId>
        <artifactId>junit-platform-launcher</artifactId>
        <version>1.0.0-M5</version>
    </dependency>
  • [ERROR] Java heap space -> [Help 1]
    [ERROR]
    [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
    [ERROR] Re-run Maven using the -X switch to enable full debug logging.
    [ERROR]
    [ERROR] For more information about the errors and possible solutions, please read the following articles:
    [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/OutOfMemoryError
    ” There was an issue with the Surefire-plugin, also documented in https://github.com/junit-team/junit5/issues/855 – upgrading the versions should help.

Appendix: Parameterized Tests with JUnit 4

This snippet is taken my blog article “New features in JUnit 4.11“…

@RunWith(Parameterized.class)
public class ParameterizedTest {
	@Parameters(name = "Run #{index}: {0}^2={1}")
	public static Iterable<Object[]> data() {
		return Arrays.asList(new Object[][] { { 1, 1 }, { 2, 4 }, { 3, 9 },
				{ 4, 16 }, { 5, 25 } });
	}
 
	private final int input;
	private final int resultExpected;
 
	public ParameterizedTest(final int input, final int result) {
		this.input = input;
		this.resultExpected = result;
	}
 
	@Test
	public void testUserMapping() {
		Calculator calc = new Calculator();
		assertEquals(resultExpected, calc.square(input));
	}
}

Tags: , , , , ,

One Response to “Using JUnit 5 Parameterized Tests, Argument Sources and Converters”

  1. Tomek Says:

    Thanks for this blog post! Parameterized tests was always one of the weaknesses of JUnit. I like the changes in ver 5, but I think I will still stick to Junitparams (http://pragmatists.github.io/JUnitParams/).

Search
Categories