Test-Driven Development with JUnit 5 Parameterized Tests

Overview

If you’re a Java developer, you most likely spend a fair amount of time writing unit tests. For those who also incorporate the principles of Test Driven Development (TDD), the health and integrity of your test code is often just as important as that of your main code. Subsequently, you may find yourself writing lots of unit tests, especially when working with complex method signatures. In order to test a variety of situations, you might be required to duplicate certain unit tests, with different input parameters being tested each time. This can cause your test classes to become bloated and difficult to maintain.

Thankfully, the JUnit 5 testing platform contains a feature that will help you eliminate this problem – Parameterized Tests. This feature will allow us to execute a single test method multiple times, while passing different input parameters each time the method is executed. In this article, we’ll explore the possibilities of the JUnit 5 parameterized testing library.

Setup

Let’s start by adding the associated dependency.

Maven:
<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.7.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>

Gradle:
dependencies {
	testCompile 'org.junit.jupiter:junit-jupiter-params:5.7.2'
}

The latest version of this dependency can be found here.

In today’s article, we will be testing the Java class RockPaperScissors:

public class RockPaperScissors {
  public String play(String player1, String player) {
    return null;
  }
}

This class contains the method public String play(), which currently returns null. We will allow our test code to drive the development of this method. All unit tests will be added to RockPaperScissorsTest:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class RockPaperScissorsTest {

  private RockPaperScissors subject;

  @BeforeEach
  public void setUp() {
    subject = new RockPaperScissors();
  }
}

As you can see, we’ve imported the @ParameterizedTest annotation. This annotation must be added to all parameterized JUnit test methods. In the next section, we’ll start creating our unit tests.

Single-parameter tests

There are several annotations that will allow us to pass a single parameter to a test method. We will first review the @ValueSource annotation. Let’s create a method to test a few different versions of “paper”.

  @ParameterizedTest
  @ValueSource(strings = {"paper", "Paper", "PAPER"})
  public void givenPlayerTwoUsesRock_returnWinningPlayer(String player1) {
    String player2 = "rock";
    String expectedResult = "Player One Wins";
    String actualResult = subject.play(player1, player2);
    assertEquals(expectedResult, actualResult);
  }

As you can see, we are using @ValueSource to pass values from a String array into the player1 parameter. This test method will be executed three times – once for each value in strings.

However, all of these tests will fail, as subject.play() will always return null. In order for our tests to pass, we’ll need to make a few changes to this method.

  public String play(String player1, String player2) {
    if (player1.toLowerCase().equals("paper")) {
      return "Player One Wins";
    } else {
      return null;
    }
  }

Now we’ll run our test method again, and we should expect to see successful tests.

Unfortunately, while @ValueSource can be very useful, it also has some limitations. We can only pass one parameter to our test method with this annotation, and the parameter must be of type primitive, String, or Class. We will discuss more robust features in the next section, multi-parameter tests.

The next annotation we’ll review is the @NullSource annotation. @NullSource will allow us to pass a single null value to a parameterized test method. I find that this annotation is best used to test the null safety of your target method. Let’s write a small unit test for this purpose.

  @ParameterizedTest
  @NullSource
  public void givenNullInput_throwRuntimeException(String nullInput) {
    String player1 = nullInput;
    String player2 = nullInput;
    String expectedResult = "invalid input";
    try {
      String tempString = subject.play(player1, player2);
    } catch (RuntimeException actualResult) {
      assertTrue(actualResult.getMessage().contains(expectedResult));
    }
  }

As you can see, our unit test is expecting subject.play() to throw a RuntimeException containing the string “invalid input”. Let’s quickly add this functionality to the play() method.

  public String play(String player1, String player2) {
    if (player1 == null || player2 == null) {
      throw new RuntimeException("invalid input - null values");
    }
    if (player1.toLowerCase().equals("paper")) {
      return "Player One Wins";
    } else {
      return null;
    }
  }

Now we’ll run this unit test. Unlike @ValueSource, @NullSource only contains one parameter, so the test method will be only be executed one time. We should see a successful test result.

The last single-parameter annotations we’ll review are the @EmptySource and @NullAndEmptySource annotations. @EmptySource will allow us to pass a single empty argument (of type String, Collection, or Array) to the annotated test method. @NullAndEmptySource will simply combine @NullSource and @EmptySource, allowing us to test both null and empty input parameters. For brevity, we’ll convert the previous @NullSource test method to instead use @NullAndEmptySource.

  @ParameterizedTest
  @NullAndEmptySource
  public void givenInvalidInput_throwRuntimeException(String invalidInput) {
    String player1 = invalidInput;
    String player2 = invalidInput;
    String expectedResult = "invalid input";
    try {
      String tempString = subject.play(player1, player2);
    } catch (RuntimeException actualResult) {
      assertTrue(actualResult.getMessage().contains(expectedResult));
    }
  }

Next, we’ll improve subject.play() with empty String safety.

  public String play(String player1, String player2) {
    if (player1 == null || player2 == null) {
      throw new RuntimeException("invalid input - null values");
    } else if (player1.isEmpty() || player2.isEmpty()) {
      throw new RuntimeException("invalid input - empty strings");
    } else if (player1.toLowerCase().equals("paper")) {
      return "Player One Wins";
    } else {
      return null;
    }
  }

Now let’s run our unit test again. The @NullAndEmptySource annotation will execute the method twice – once with a null input parameter, and once with an empty input parameter.

As you can see, both of our unit tests are successful. Our target method is able to handle both null and empty values. In the next section, we’ll take a look at some multi-parameter annotations that will help us write more complex unit tests.

Multi-parameter tests

In the previous section, we examined several single-parameter annotations. These annotations allow a single input parameter to be passed into the annotated test method. Unfortunately, unit tests created in this way will have many limitations. In our current RockPaperScissors example, we are only able to test one player’s move. However, if we use multi-parameter annotations, our unit tests can be improved considerably. The first annotation we’ll take a look at is @CsvSource. This annotation will accept an array of comma-separated values, and it will allow us to pass multiple parameters into our test method.

  @ParameterizedTest
  @CsvSource({"paper,rock", "rock,scissors", "scissors,paper"})
  public void givenWinningMoveByPlayerOne_returnPlayerOneWins(String player1, String player2) {
    String expectedResult = "Player One Wins";
    String actualResult = subject.play(player1, player2);
    assertEquals(expectedResult, actualResult);
  }

Let’s run this test method and see what happens.

As you can see, only the first test case succeeded. This is because our target method has been hard-coded to return “Player One Wins” when Player One uses “paper”. Let’s refactor subject.play().

public class RockPaperScissors {

  private static final String[][] winningMoves =
      new String[][]{{"paper", "rock"}, {"rock", "scissors"}, {"scissors", "paper"}};

  public String play(String player1, String player2) {

    if (player1 == null || player2 == null) {
      throw new RuntimeException("invalid input - null values");
    } else if (player1.isEmpty() || player2.isEmpty()) {
      throw new RuntimeException("invalid input - empty strings");
    } else if (player1.toLowerCase().equals(player2.toLowerCase())) {
      return "Draw";
    }

    for (String[] winningMove : winningMoves) {
      if (player1.toLowerCase().equals(winningMove[0]) 
          && player2.toLowerCase().equals(winningMove[1])) {
        return "Player One Wins";
      } else if (player2.toLowerCase().equals(winningMove[0]) 
          && player1.toLowerCase().equals(winningMove[1])) {
        return "Player Two Wins";
      }
    }
    throw new RuntimeException(“Unable to determine the winner”);
  }
}

As you can see, our target method has been improved significantly. It should be able to determine if Player One or Player Two wins, or if the result is a Draw. Let’s run our test method again.

As you can see, all of our unit tests were successful. Our target method correctly chooses Player One as the winner each time the test method is executed. However, we are still hard-coding our expected result, and we are only passing method parameters that conform to the “PlayerOneWins” scenario. Let’s see if we can make a more dynamic unit test.

  @ParameterizedTest
  @CsvSource(delimiter = ':', value = {
      "paper:rock:Player One Wins",
      "rock:rock:Draw",
      "scissors:rock:Player Two Wins"
  })
  public void givenValidMoves_determineWinner(
      String player1, String player2, String expectedResult) {
    String actualResult = subject.play(player1, player2);
    assertEquals(expectedResult, actualResult);
  }

As you can see, @CsvSource enables us to test more than two parameters. Our annotated test method will receive values for player1, player2, and expectedResult. Let’s run our new test method and see what happens.

As you can see, our unit tests were successful. We’ve made our test method much more dynamic.

The next annotation we’ll review is the @CsvFileSource annotation. This annotation is similar to the @CsvSource annotation. However, instead of using comma-separated values from a literal array, @CsvFileSource will retrieve values from a .csv file. Let’s take a look.

  @ParameterizedTest
  @CsvFileSource(resources = "/player-moves.csv", numLinesToSkip = 1)
  public void givenValidMovesFromFile_determineWinner(
      String player1, String player2, String expectedResult) {
    String actualResult = subject.play(player1, player2);
    assertEquals(expectedResult, actualResult);
  }

As you can see, this annotation will retrieve the comma-separated values from a *specified file path. We are also able to skip lines – the header row in our .csv file in this case. We’ll take a look at this file next.

*The resources parameter can be a bit tricky to use. If you are using IntelliJ, the working directory used by this parameter should be present in the modules section under “Test Resource Folders” – for me that’s src/test/resources.

Before we execute our new unit test, let’s take a look at the contents of our .csv file.

Player One,Player Two,Expected Result
rock,paper,Player Two Wins
paper,scissors,Player Two Wins
scissors,rock,Player Two Wins
scissors,scissors,Draw
paper,paper,Draw
rock,rock,Draw
paper,rock,Player One Wins
rock,scissors,Player One Wins
scissors,paper,Player One Wins

As you can see, there are 10 rows of data, with the first row containing headers. The csv data in the non-header rows represents a list of all possible RockPaperScissors outcomes. Let’s see if our unit tests are successful.

As you can see, our test method was able to test all nine possibilities using @CsvFileSource. Our target method is able to produce the desired result for all valid move combinations. However, we still have a few more features to test out. Let’s see if we can improve our test code using the ArgumentsAccessor class. This feature will allow us to reduce the number of parameters in our test method signatures by encapsulating the within an instance of ArgumentsAccessor. Let’s take a look.

  @ParameterizedTest
  @CsvFileSource(resources = "/player-moves.csv", numLinesToSkip = 1)
  public void givenValidMoves_determineWinner_withAccessor(ArgumentsAccessor argumentsAccessor) {
    String player1 = argumentsAccessor.getString(0);
    String player2 = argumentsAccessor.getString(1);
    String expectedResult = argumentsAccessor.getString(2);
    String actualResult = subject.play(player1, player2);
    assertEquals(expectedResult, actualResult);
  }

As you can see, this method does require a bit more code. It might not seem like much of an improvement when only using three method parameters. However, when using a large number of parameters, this method will greatly simplify your method signatures.

Enumerable-parameter tests

Before we wrap up, I’d like to quickly review the @EnumSource annotation. Unfortunately, this annotation will only allow us to pass a single parameter to the annotated test method. However, while this method might not be very useful in our current test case, it can certainly be useful elsewhere. Let’s take a look.

  @ParameterizedTest
  @EnumSource(ValidMove.class)
  public void givenDuplicateMoves_returnDraw(ValidMove validMove) {
    String player1 = validMove.name();
    String player2 = validMove.name();
    String expectedResult = "Draw";
    String actualResult = subject.play(player1, player2);
    assertEquals(expectedResult, actualResult);
  }

As you can see, @EnumSource allows us to specify an Enum class that contains the parameter values we’d like to test – in this case, we can create a class called ValidMove that contains all the valid moves for RockPaperScissors.

public enum ValidMove {
  rock,
  paper,
  scissors,
  Rock,
  Paper,
  Scissors,
  ROCK,
  PAPER,
  SCISSORS
}

Let’s run our test method.

As you can see, all of our unit tests were successful.

Conclusion

In this article, we’ve reviewed several methods for creating parameterized unit tests with JUnit 5, including the @ValueSource, @CsvSource, and @CsvFileSource annotations. By using these features, you can greatly reduce the verbosity of your test code without sacrificing application integrity. This streamlined approach to unit testing works perfectly when practicing test-driven development, producing simple and efficient test methods. As always, there’s much more to learn about this library – you can check out the documentation here. Happy testing!