Java > Testing in Java > Unit Testing > Parameterized Tests

Parameterized Unit Testing with JUnit 5

This snippet demonstrates how to use JUnit 5's parameterized tests to run the same test multiple times with different inputs. This is particularly useful for testing boundary conditions, different data types, and a range of values for a single method or functionality. JUnit 5 offers several ways to provide these parameters, including ValueSource, MethodSource, and CsvSource, making parameterized testing a flexible and powerful tool in your testing arsenal.

Setting up JUnit 5 Dependency

Before you start, make sure you have JUnit 5 dependencies in your project. The snippets above show how to add JUnit 5 to your Maven or Gradle project. Replace `5.10.0` with the latest stable version.

<!-- Maven -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>
<!-- Gradle -->
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'

Example: Parameterized Test with ValueSource

This example demonstrates using @ValueSource to provide different string values to the isPalindrome method in the StringUtils class. The @ParameterizedTest annotation marks the method as a parameterized test. The @ValueSource annotation specifies an array of strings to be used as input for each test run. The isPalindrome method checks if a given string is a palindrome, ignoring non-alphanumeric characters and case.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;

class StringUtils {
    static boolean isPalindrome(String input) {
        String cleanInput = input.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
        String reversedInput = new StringBuilder(cleanInput).reverse().toString();
        return cleanInput.equals(reversedInput);
    }
}

class PalindromeTest {

    @ParameterizedTest
    @ValueSource(strings = {"racecar", "A man, a plan, a canal: Panama", "madam"})
    void isPalindrome_ShouldReturnTrueForPalindromes(String candidate) {
        assertTrue(StringUtils.isPalindrome(candidate));
    }

    @ParameterizedTest
    @ValueSource(strings = {"hello", "world", "java"})
    void isPalindrome_ShouldReturnFalseForNonPalindromes(String candidate) {
        assertFalse(StringUtils.isPalindrome(candidate));
    }
}

Example: Parameterized Test with MethodSource

This example demonstrates using @MethodSource to provide parameters to a parameterized test. The additionExamples method returns a Stream of Object arrays, where each array contains the input values and the expected result for the add method of the Calculator class. The @MethodSource annotation specifies the name of the method that provides the parameters. This allows for more complex parameter generation than @ValueSource.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;

class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

class CalculatorTest {
    private final Calculator calculator = new Calculator();

    @ParameterizedTest
    @MethodSource("additionExamples")
    void add_ShouldReturnCorrectSum(int a, int b, int expected) {
        assertEquals(expected, calculator.add(a, b));
    }

    static Stream<Object[]> additionExamples() {
        return Stream.of(
                new Object[]{1, 2, 3},
                new Object[]{5, 5, 10},
                new Object[]{10, -3, 7}
        );
    }
}

Example: Parameterized Test with CsvSource

This example demonstrates using @CsvSource to provide parameters to a parameterized test in a comma-separated value format. Each line in the @CsvSource annotation represents a set of input values and the expected result. The calculateLength_ShouldReturnCorrectLength test method takes the input string and the expected length as parameters, and asserts that the calculateLength method returns the correct length.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;

class StringLength {
    static int calculateLength(String input) {
        return input.length();
    }
}

class StringLengthTest {

    @ParameterizedTest
    @CsvSource({
            "hello, 5",
            "world, 5",
            "java, 4",
            "'', 0"  // Empty string
    })
    void calculateLength_ShouldReturnCorrectLength(String input, int expectedLength) {
        assertEquals(expectedLength, StringLength.calculateLength(input));
    }
}

Concepts Behind Parameterized Testing

Parameterized testing involves running the same test logic multiple times with different sets of input data. This reduces code duplication and makes tests more concise and easier to maintain. JUnit 5's parameterized test feature enables developers to provide different sources for the test parameters, making it a powerful tool for comprehensive testing.

Real-Life Use Case

Consider a validation function that needs to verify if an email address is valid based on several rules. Instead of writing separate test methods for each rule, you can create a parameterized test with a CSV source containing different email addresses and their expected validity. This approach significantly reduces code duplication and makes the test suite more maintainable.

Best Practices

  • Keep the test logic simple and focused. The goal of parameterized tests is to verify the behavior of the code with different inputs, not to test complex logic within the test itself.
  • Use meaningful names for the test methods and parameters to improve readability.
  • Choose the appropriate parameter source based on the complexity of the data. @ValueSource is suitable for simple values, while @MethodSource or @CsvSource are better for more complex data sets.
  • Ensure that the test data covers a wide range of possible inputs, including edge cases and boundary conditions.

Interview Tip

Be prepared to explain the benefits of parameterized testing over traditional unit testing, such as reduced code duplication and improved test coverage. Also, be ready to discuss the different types of parameter sources available in JUnit 5 and when to use each one.

When to Use Them

Parameterized tests are beneficial when you need to test the same code with a variety of inputs and expected outputs. They are especially useful for testing boundary conditions, validating input data, and ensuring that code handles different data types correctly.

Memory Footprint

Parameterized tests may have a slightly larger memory footprint than traditional unit tests because they need to store the test data in memory. However, the memory overhead is usually minimal and should not be a concern unless you are dealing with extremely large datasets.

Alternatives

  • Table-Driven Testing: This approach involves creating a table of inputs and expected outputs and then iterating over the table to run the tests. This can be achieved without using specific parameterized testing frameworks, but it requires more manual code.
  • Data-Driven Testing (using external files): Parameters can be loaded from external files like CSV or Excel. This can be useful when data is large or can be managed by non-programmers.

Pros

  • Reduced Code Duplication: Avoid writing similar test methods for different inputs.
  • Improved Test Coverage: Easily test a wider range of inputs and edge cases.
  • Increased Readability: Tests become more concise and easier to understand.
  • Enhanced Maintainability: Easier to update and maintain tests when the logic remains the same but the input data changes.

Cons

  • Increased Complexity: Parameterized tests can be more complex to set up than traditional unit tests, especially when using @MethodSource or @CsvSource.
  • Potential for Errors: If the test data is not carefully chosen, it can lead to incomplete or misleading test results.
  • Debugging Challenges: Identifying the exact input that caused a test failure can sometimes be more challenging in parameterized tests.

FAQ

  • What is the main advantage of using Parameterized Tests?

    The main advantage is reducing code duplication. Instead of writing multiple similar test methods with different inputs, you can write a single parameterized test that runs with multiple sets of data.
  • How do I provide data to a Parameterized Test?

    JUnit 5 provides several annotations for supplying data: @ValueSource for simple values, @MethodSource for data generated by a method, and @CsvSource for data in CSV format.
  • Can I use Parameterized Tests with Spring Boot?

    Yes, you can use Parameterized Tests with Spring Boot. Make sure to include the necessary JUnit 5 and Spring Test dependencies in your project.
  • How do I handle null values in Parameterized Tests?

    You can use @NullSource or @NullAndEmptySource annotations to provide null or empty string values as input to your parameterized tests.