unit-test-parameterized · git:20260316.91dc3c4 · 2026-03-16 · sha256 d5b2402ac86c1e6d

unit-test-parameterized git:20260316.91dc3c4A

Immutable. This exact content is served forever at /api/v1/blob/d5b2402ac86c1e6d.

---
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.
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({
    "alice@example.com, true",
    "bob@gmail.com, true",
    "invalid-email, false",
    "user@, false",
    "@example.com, false",
    "user name@example.com, 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)