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-parameterizedReview the files below or copy the command above to add this skill to your agents.
---
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)
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.
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.
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.