Parameterized Tests in JUnit: Writing Data-Driven Tests

Illustration for Parameterized Tests in JUnit: Writing Data-Driven Tests
By Last updated:

One of the most powerful features in modern testing frameworks is the ability to write data-driven tests. Instead of duplicating test logic for multiple inputs, JUnit 5 provides parameterized tests that let you reuse a single test method with different sets of data. This approach reduces code duplication, improves readability, and ensures broader test coverage.

In this tutorial, we’ll explore parameterized testing in JUnit 5 using @ValueSource, @CsvSource, @CsvFileSource, and @MethodSource, complete with examples, real-world applications, and best practices.


Why Parameterized Tests Matter

  • Efficiency: Replace repetitive test methods with one parameterized method.
  • Maintainability: Easier to add new test cases by simply extending data sources.
  • Coverage: Test more inputs with less code.
  • CI/CD Ready: Scales easily for automated builds and pipelines.

Think of parameterized tests like templates: instead of writing multiple versions of the same test, you fill in the blanks with different data.


Basic Setup for Parameterized Tests

To use parameterized tests, include the following Maven dependency:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
</dependency>

Gradle:

testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0'

@ValueSource — Single Input Values

Use @ValueSource to run the same test with multiple primitive or string values.

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

class PalindromeTest {

    @ParameterizedTest
    @ValueSource(strings = {"racecar", "radar", "level"})
    void testPalindrome(String word) {
        assertTrue(isPalindrome(word));
    }

    boolean isPalindrome(String word) {
        return word.equals(new StringBuilder(word).reverse().toString());
    }
}

@CsvSource — Inline Multiple Arguments

Use @CsvSource for test methods requiring multiple inputs.

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

class CalculatorTest {

    @ParameterizedTest
    @CsvSource({
        "2, 3, 5",
        "10, 5, 15",
        "7, 8, 15"
    })
    void testAddition(int a, int b, int expected) {
        Calculator calc = new Calculator();
        assertEquals(expected, calc.add(a, b));
    }
}

@CsvFileSource — Externalized Test Data

Use @CsvFileSource to load test data from an external CSV file.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

class DiscountCalculatorTest {

    @ParameterizedTest
    @CsvFileSource(resources = "/discount-data.csv", numLinesToSkip = 1)
    void testDiscounts(double price, double discount, double expected) {
        DiscountCalculator dc = new DiscountCalculator();
        assertEquals(expected, dc.applyDiscount(price, discount));
    }
}

discount-data.csv

price,discount,expected
100,0.1,90
200,0.25,150
50,0.5,25

@MethodSource — Complex Test Data

Use @MethodSource to generate complex or dynamic data.

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.assertEquals;

class FactorialTest {

    static Stream<org.junit.jupiter.params.provider.Arguments> provideFactorials() {
        return Stream.of(
            org.junit.jupiter.params.provider.Arguments.of(0, 1),
            org.junit.jupiter.params.provider.Arguments.of(1, 1),
            org.junit.jupiter.params.provider.Arguments.of(5, 120)
        );
    }

    @ParameterizedTest
    @MethodSource("provideFactorials")
    void testFactorial(int input, int expected) {
        assertEquals(expected, factorial(input));
    }

    int factorial(int n) {
        return (n <= 1) ? 1 : n * factorial(n - 1);
    }
}

Real-World Use Cases

  1. Validation Logic: Check multiple email or username formats.
  2. Mathematical Functions: Test algorithms like factorial, prime checks, or Fibonacci.
  3. REST APIs: Verify API endpoints with multiple request/response payloads.
  4. Discounts/Business Rules: Ensure financial calculations work across multiple scenarios.

Best Practices for Parameterized Tests

  • Keep test methods focused — one concern per test.
  • Use descriptive display names with @DisplayName or @DisplayNameGeneration.
  • Externalize large datasets with @CsvFileSource.
  • Combine with Mockito for mocking dependencies per input.
  • Avoid overloading tests with too many parameters — prefer clarity.

Advanced Testing & CI/CD

  • TDD: Write parameterized tests upfront to drive algorithm design.
  • CI/CD: Parameterized tests scale well in Jenkins, GitHub Actions, and GitLab pipelines.
  • Testcontainers: Combine with parameterized tests for database-driven scenarios.
  • JaCoCo: Measure coverage to ensure all input variations are tested.

Version Tracker

  • JUnit 4 → JUnit 5: Parameterized tests significantly improved — no need for external libraries like JUnitParams.
  • Mockito Updates: Modern features enable mocking within parameterized tests.
  • Testcontainers Growth: Use parameterized tests with containers for DB, Kafka, and microservices validation.

Conclusion & Key Takeaways

Parameterized tests in JUnit 5 simplify data-driven testing by reusing test logic with multiple inputs. They reduce duplication, increase coverage, and integrate seamlessly with CI/CD pipelines.

Key Takeaways:

  • Use @ValueSource for simple single-argument tests.
  • Use @CsvSource or @CsvFileSource for multiple arguments or externalized data.
  • Use @MethodSource for complex or programmatic datasets.
  • Parameterized tests are essential for scalable, maintainable test suites.

FAQ

1. What is the difference between @CsvSource and @CsvFileSource?
@CsvSource uses inline data, while @CsvFileSource loads data from an external CSV file.

2. Can I use Mockito with parameterized tests?
Yes, mocks and stubs can be injected into parameterized test methods.

3. Do parameterized tests support multiple arguments?
Yes, using @CsvSource, @CsvFileSource, or @MethodSource.

4. Can parameterized tests work with dynamic inputs?
Yes, use @MethodSource to generate data streams at runtime.

5. Are parameterized tests slower than normal tests?
They may take longer due to multiple executions but provide broader coverage.

6. Can I use @ParameterizedTest with assertions like assertThrows?
Yes, parameterized tests work with all assertion methods.

7. How do parameterized tests improve maintainability?
They reduce duplication and centralize test logic.

8. Do parameterized tests work in CI/CD pipelines?
Yes, they integrate seamlessly with build tools and CI/CD systems.

9. Can I reuse data sources across multiple test classes?
Yes, by defining static providers and referencing them with @MethodSource.

10. Should I migrate from JUnit 4 parameterized tests?
Yes, JUnit 5 offers cleaner, more powerful parameterized test support.