home / skills / giuseppe-trisciuoglio / developer-kit / unit-test-parameterized

This skill guides you in writing parameterized unit tests with JUnit 5 using various sources to cover multiple scenarios efficiently.

npx playbooks add skill giuseppe-trisciuoglio/developer-kit --skill unit-test-parameterized

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
11.9 KB
---
name: unit-test-parameterized
description: Provides parameterized testing patterns with @ParameterizedTest, @ValueSource, @CsvSource. Enables running a single test method with multiple input combinations. Use when testing multiple scenarios with similar logic.
category: testing
tags: [junit-5, parameterized-test, value-source, csv-source, method-source]
version: 1.0.1
allowed-tools: Read, Write, Bash, Glob, Grep
---

# Parameterized Unit Tests with JUnit 5

## Overview

This skill provides patterns for writing efficient parameterized unit tests using JUnit 5's @ParameterizedTest. It covers @ValueSource, @CsvSource, @MethodSource, @EnumSource, @ArgumentsSource, and custom display names to run the same test logic with multiple input values, reducing test duplication and improving coverage.

## When to Use

Use this skill when:
- Testing methods with multiple valid inputs
- Testing boundary values systematically
- Testing multiple invalid inputs for error cases
- Want to reduce test duplication
- Testing multiple scenarios with similar assertions
- Need data-driven testing approach

## Instructions

1. **Add junit-jupiter-params dependency**: Ensure junit-jupiter-params is on test classpath
2. **Choose appropriate source**: Use @ValueSource for simple values, @CsvSource for tabular data, @MethodSource for complex objects
3. **Match parameter types**: Ensure test method parameters match data source types
4. **Use descriptive display names**: Set `name = "..."` for readable test output
5. **Test boundary values**: Include edge cases, null values, and extreme values in parameters
6. **Use @EnumSource**: Test all enum values or filter specific ones
7. **Create custom ArgumentsProvider**: Build reusable data sources for complex scenarios
8. **Keep assertions simple**: Focus on single assertion per parameterized test

## Examples

## Setup: Parameterized Testing

### Maven
```xml
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <scope>test</scope>
</dependency>
```

### Gradle
```kotlin
dependencies {
  testImplementation("org.junit.jupiter:junit-jupiter")
  testImplementation("org.assertj:assertj-core")
}
```

## Basic Pattern: @ValueSource

### Simple Value Testing

```java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.*;

class StringUtilsTest {

  @ParameterizedTest
  @ValueSource(strings = {"hello", "world", "test"})
  void shouldCapitalizeAllStrings(String input) {
    String result = StringUtils.capitalize(input);
    assertThat(result).startsWith(input.substring(0, 1).toUpperCase());
  }

  @ParameterizedTest
  @ValueSource(ints = {1, 2, 3, 4, 5})
  void shouldBePositive(int number) {
    assertThat(number).isPositive();
  }

  @ParameterizedTest
  @ValueSource(booleans = {true, false})
  void shouldHandleBothBooleanValues(boolean value) {
    assertThat(value).isNotNull();
  }
}
```

## @MethodSource for Complex Data

### Factory Method Data Source

```java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;

class CalculatorTest {

  static Stream<org.junit.jupiter.params.provider.Arguments> additionTestCases() {
    return Stream.of(
      Arguments.of(1, 2, 3),
      Arguments.of(0, 0, 0),
      Arguments.of(-1, 1, 0),
      Arguments.of(100, 200, 300),
      Arguments.of(-5, -10, -15)
    );
  }

  @ParameterizedTest
  @MethodSource("additionTestCases")
  void shouldAddNumbersCorrectly(int a, int b, int expected) {
    int result = Calculator.add(a, b);
    assertThat(result).isEqualTo(expected);
  }
}
```

## @CsvSource for Tabular Data

### CSV-Based Test Data

```java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class UserValidationTest {

  @ParameterizedTest
  @CsvSource({
    "[email protected], true",
    "[email protected], true",
    "invalid-email, false",
    "user@, false",
    "@example.com, false",
    "user [email protected], false"
  })
  void shouldValidateEmailAddresses(String email, boolean expected) {
    boolean result = UserValidator.isValidEmail(email);
    assertThat(result).isEqualTo(expected);
  }

  @ParameterizedTest
  @CsvSource({
    "123-456-7890, true",
    "555-123-4567, true",
    "1234567890, false",
    "123-45-6789, false",
    "abc-def-ghij, false"
  })
  void shouldValidatePhoneNumbers(String phone, boolean expected) {
    boolean result = PhoneValidator.isValid(phone);
    assertThat(result).isEqualTo(expected);
  }
}
```

## @CsvFileSource for External Data

### CSV File-Based Testing

```java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;

class PriceCalculationTest {

  @ParameterizedTest
  @CsvFileSource(resources = "/test-data/prices.csv", numLinesToSkip = 1)
  void shouldCalculateTotalPrice(String product, double price, int quantity, double expected) {
    double total = PriceCalculator.calculateTotal(price, quantity);
    assertThat(total).isEqualTo(expected);
  }
}

// test-data/prices.csv:
// product,price,quantity,expected
// Laptop,999.99,1,999.99
// Mouse,29.99,3,89.97
// Keyboard,79.99,2,159.98
```

## @EnumSource for Enum Testing

### Enum-Based Test Data

```java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

enum Status { ACTIVE, INACTIVE, PENDING, DELETED }

class StatusHandlerTest {

  @ParameterizedTest
  @EnumSource(Status.class)
  void shouldHandleAllStatuses(Status status) {
    assertThat(status).isNotNull();
  }

  @ParameterizedTest
  @EnumSource(value = Status.class, names = {"ACTIVE", "INACTIVE"})
  void shouldHandleSpecificStatuses(Status status) {
    assertThat(status).isIn(Status.ACTIVE, Status.INACTIVE);
  }

  @ParameterizedTest
  @EnumSource(value = Status.class, mode = EnumSource.Mode.EXCLUDE, names = {"DELETED"})
  void shouldHandleStatusesExcludingDeleted(Status status) {
    assertThat(status).isNotEqualTo(Status.DELETED);
  }
}
```

## Custom Display Names

### Readable Test Output

```java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class DiscountCalculationTest {

  @ParameterizedTest(name = "Discount of {0}% should be calculated correctly")
  @ValueSource(ints = {5, 10, 15, 20})
  void shouldApplyDiscount(int discountPercent) {
    double originalPrice = 100.0;
    double discounted = DiscountCalculator.apply(originalPrice, discountPercent);
    double expected = originalPrice * (1 - discountPercent / 100.0);
    
    assertThat(discounted).isEqualTo(expected);
  }

  @ParameterizedTest(name = "User role {0} should have {1} permissions")
  @CsvSource({
    "ADMIN, 100",
    "MANAGER, 50",
    "USER, 10"
  })
  void shouldHaveCorrectPermissions(String role, int expectedPermissions) {
    User user = new User(role);
    assertThat(user.getPermissionCount()).isEqualTo(expectedPermissions);
  }
}
```

## Combining Multiple Sources

### ArgumentsProvider for Complex Scenarios

```java
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;
import java.util.stream.Stream;

class RangeValidatorArgumentProvider implements ArgumentsProvider {
  @Override
  public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
    return Stream.of(
      Arguments.of(0, 0, 100, true),      // Min boundary
      Arguments.of(100, 0, 100, true),    // Max boundary
      Arguments.of(50, 0, 100, true),     // Middle value
      Arguments.of(-1, 0, 100, false),    // Below range
      Arguments.of(101, 0, 100, false)    // Above range
    );
  }
}

class RangeValidatorTest {

  @ParameterizedTest
  @ArgumentsSource(RangeValidatorArgumentProvider.class)
  void shouldValidateRangeCorrectly(int value, int min, int max, boolean expected) {
    boolean result = RangeValidator.isInRange(value, min, max);
    assertThat(result).isEqualTo(expected);
  }
}
```

## Testing Edge Cases with Parameters

### Boundary Value Analysis

```java
class BoundaryValueTest {

  @ParameterizedTest
  @ValueSource(ints = {
    Integer.MIN_VALUE,    // Absolute minimum
    Integer.MIN_VALUE + 1, // Just above minimum
    -1,                    // Negative boundary
    0,                     // Zero boundary
    1,                     // Just above zero
    Integer.MAX_VALUE - 1, // Just below maximum
    Integer.MAX_VALUE      // Absolute maximum
  })
  void shouldHandleAllBoundaryValues(int value) {
    int incremented = MathUtils.increment(value);
    assertThat(incremented).isNotLessThan(value);
  }

  @ParameterizedTest
  @CsvSource({
    ",                    false", // null
    "'',                   false", // empty
    "'   ',                false", // whitespace only
    "a,                    true",  // single character
    "abc,                  true"   // normal
  })
  void shouldValidateStrings(String input, boolean expected) {
    boolean result = StringValidator.isValid(input);
    assertThat(result).isEqualTo(expected);
  }
}
```

## Repeat Tests

### Run Same Test Multiple Times

```java
import org.junit.jupiter.api.RepeatedTest;

class ConcurrencyTest {

  @RepeatedTest(100)
  void shouldHandleConcurrentAccess() {
    // Test that might reveal race conditions if run multiple times
    AtomicInteger counter = new AtomicInteger(0);
    counter.incrementAndGet();
    assertThat(counter.get()).isEqualTo(1);
  }
}
```

## Best Practices

- **Use @ParameterizedTest** to reduce test duplication
- **Use descriptive display names** with `(name = "...")`
- **Test boundary values** systematically
- **Keep test logic simple** - focus on single assertion
- **Organize test data logically** - group similar scenarios
- **Use @MethodSource** for complex test data
- **Use @CsvSource** for tabular test data
- **Document expected behavior** in test names

## Common Patterns

**Testing error conditions**:
```java
@ParameterizedTest
@ValueSource(strings = {"", " ", null})
void shouldThrowExceptionForInvalidInput(String input) {
  assertThatThrownBy(() -> Parser.parse(input))
    .isInstanceOf(IllegalArgumentException.class);
}
```

**Testing multiple valid inputs**:
```java
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 5, 8, 13})
void shouldBeInFibonacciSequence(int number) {
  assertThat(FibonacciChecker.isFibonacci(number)).isTrue();
}
```

## Constraints and Warnings

- **Parameter count must match**: The number of parameters from source must match test method signature
- **Type conversion is automatic**: JUnit converts source values to target parameter types when possible
- **@ValueSource limitation**: Only supports literals (strings, ints, longs, doubles); not objects or null
- **CSV escaping**: Strings containing commas must be enclosed in single quotes in @CsvSource
- **MethodSource visibility**: @MethodSource methods must be static, can be private but must be in same class
- **Display name placeholders**: Use {0}, {1}, etc. to reference parameters in display names
- **Test execution order**: Parameterized tests execute each parameter set as a separate test invocation

## Troubleshooting

**Parameter not matching**: Verify number and type of parameters match test method signature.

**Display name not showing**: Check parameter syntax in `name = "..."`.

**CSV parsing error**: Ensure CSV format is correct and quote strings containing commas.

## References

- [JUnit 5 Parameterized Tests](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests)
- [@ParameterizedTest Documentation](https://junit.org/junit5/docs/current/api/org.junit.jupiter.params/org/junit/jupiter/params/ParameterizedTest.html)
- [Boundary Value Analysis](https://en.wikipedia.org/wiki/Boundary-value_analysis)

Overview

This skill provides practical patterns for writing parameterized unit tests with JUnit 5 using @ParameterizedTest, @ValueSource, @CsvSource, @MethodSource, @EnumSource and custom providers. It helps run the same test logic with multiple input sets to reduce duplication and increase coverage. The guidance covers setup, common sources, display names, and edge-case strategies.

How this skill works

Add the junit-jupiter-params dependency and annotate test methods with @ParameterizedTest. Attach a data source annotation (@ValueSource, @CsvSource, @MethodSource, @EnumSource, @ArgumentsSource or @CsvFileSource) so JUnit automatically invokes the test once per data row. Use static factory methods or custom ArgumentsProvider for complex objects and names with placeholders for readable output.

When to use it

  • When a method must be validated across many input values without duplicating test code
  • When you need systematic boundary and edge-case coverage (including nulls and extremes)
  • When validating tabular inputs or combinations of arguments
  • When testing all or filtered enum values
  • When building reusable, data-driven tests for business rules or validators

Best practices

  • Keep each parameterized test focused on a single assertion or behavioral expectation
  • Choose the simplest data source: @ValueSource for literals, @CsvSource for tabular, @MethodSource for objects
  • Include boundary and invalid inputs alongside normal cases
  • Use descriptive display names with placeholders (e.g. name = "{0} => expected {1}")
  • Group related scenarios and prefer reusable ArgumentsProvider for complex sets

Example use cases

  • Validate email or phone validators with @CsvSource rows mapping input to expected boolean
  • Verify numeric math logic across positive, negative and extreme integers with @ValueSource or @MethodSource
  • Run the same business rule for every enum state using @EnumSource
  • Supply complex DTOs or many arguments using a static factory method and @MethodSource
  • Load test cases from CSV files with @CsvFileSource for large datasets

FAQ

What if my test method parameters don't match the data source?

Ensure the number and types of parameters in the method signature match the values provided by the source. Use Arguments.of(...) in MethodSource to align types.

Can I include null values in @CsvSource or @ValueSource?

ValueSource does not support null or complex objects. Use @CsvSource with special syntax (empty token or explicit markers) or @MethodSource to produce nulls and objects.