Writing Scalable Parameterized Tests in JUnit 5

Snippet of programming code in IDE
Published on

Writing Scalable Parameterized Tests in JUnit 5

When it comes to testing in Java, JUnit is a widely-used framework due to its simplicity and effectiveness. In this blog post, we'll explore how to write scalable parameterized tests in JUnit 5. Parameterized tests allow us to run the same test with different inputs, which is essential when dealing with repetitive test cases.

What are parameterized tests?

In traditional unit tests, you write a test method for each individual case you want to test. But what if you have a set of similar test cases that only differ in their input values? This is where parameterized tests come in handy. They allow you to write a single test method and run it with different input values.

Setting up the project

Before we dive into writing parameterized tests, let's set up a simple Java project with JUnit 5.

// pom.xml
<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.7.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Once you have the JUnit 5 dependency in your project, you're ready to start writing parameterized tests.

Writing parameterized tests in JUnit 5

In JUnit 5, parameterized tests are defined using the @ParameterizedTest annotation. Let's say we want to test a simple Calculator class that has a method to add two numbers. We'll write a parameterized test to test the add method with multiple inputs.

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

// CalculatorTest.java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

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

    @ParameterizedTest
    @CsvSource({"1, 1, 2", "2, 3, 5", "5, 5, 10"})
    void testAdd(int a, int b, int expected) {
        assertEquals(expected, calculator.add(a, b));
    }
}

In the above example, we use the @CsvSource annotation to define the input values for the test method. Each row in the CSV represents a set of input values, followed by the expected result. The test method is then annotated with @ParameterizedTest and takes parameters corresponding to the input values.

Using method source for parameterized tests

Instead of using @CsvSource, you can also use a method that returns a Stream of arguments for parameterized tests. This is useful when you have a large set of input values or when you need to generate them dynamically.

// CalculatorTest.java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

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

    static Stream<Arguments> addProvider() {
        return Stream.of(
            Arguments.of(1, 1, 2),
            Arguments.of(2, 3, 5),
            Arguments.of(5, 5, 10)
        );
    }

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

In the above example, the addProvider method returns a Stream of Arguments, each representing a set of input values. The @MethodSource annotation is then used to specify the method as the source for the test arguments.

Custom argument providers

Sometimes, you may need to generate test arguments dynamically or from a different source. In such cases, you can create custom argument providers by implementing the ArgumentsProvider interface.

// RandomNumbersProvider.java
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;

import java.util.stream.Stream;

public class RandomNumbersProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
            Arguments.of(3, 4, 7),
            Arguments.of(10, 20, 30),
            Arguments.of(5, 5, 10)
        );
    }
}

// CalculatorTest.java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

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

    @ParameterizedTest
    @ArgumentsSource(RandomNumbersProvider.class)
    void testAdd(int a, int b, int expected) {
        assertEquals(expected, calculator.add(a, b));
    }
}

In this example, RandomNumbersProvider implements the ArgumentsProvider interface and provides the test arguments dynamically. The @ArgumentsSource annotation is then used to specify the custom argument provider for the parameterized test.

Closing the Chapter

In this blog post, we've explored how to write scalable parameterized tests in JUnit 5. We've covered using @CsvSource and @MethodSource for providing test arguments, as well as creating custom argument providers. Parameterized tests are an essential tool in any test suite, allowing you to write concise and maintainable tests for repetitive scenarios.

By leveraging the flexibility and reusability of parameterized tests in JUnit 5, you can streamline your testing process, making it more efficient and effective. With this knowledge, you can write tests that scale with your application, ensuring its reliability and robustness.

Start incorporating parameterized tests in your JUnit 5 test suite and experience the benefits of writing cleaner, more expressive tests with less repetitive code. Happy testing!

Remember, the most effective tests are those that provide meaningful coverage of your code and help you catch potential bugs before they become issues in the production environment. Happy testing!