unit-test-parameterized · diff
git:20260316.91dc3c4 to git:20260324.a02e282
98 added, 299 removed. Audit A to A.
---
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.
+ description: Provides parameterized testing patterns with JUnit 5, generates data-driven unit tests using @ParameterizedTest, @ValueSource, @CsvSource, @MethodSource. Creates tests that run the same logic with multiple input values. Use when writing data-driven Java tests, multiple test cases from single method, or boundary value analysis.
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.
+ Provides patterns for parameterized unit tests in Java using JUnit 5. Covers `@ValueSource`, `@CsvSource`, `@MethodSource`, `@EnumSource`, `@ArgumentsSource`, and custom display names. Reduces test duplication by running the same test logic with multiple input values.
## 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
+ - Writing JUnit tests with multiple input combinations
+ - Implementing data-driven tests in Java
+ - Running same test with different values (boundary analysis)
+ - Testing multiple scenarios from single test method
## 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
+ 1. **Add dependency**: Ensure `junit-jupiter-params` is on test classpath (included in `junit-jupiter`)
+ 2. **Choose source**: `@ValueSource` for simple values, `@CsvSource` for tabular data, `@MethodSource` for complex objects
+ 3. **Match parameters**: Test method parameters must match data source types
+ 4. **Set display names**: Use `name = "{0}..."` for readable output
+ 5. **Validate**: Run `./gradlew test --info` or `mvn test` and verify all parameter combinations execute
## Examples
- ## Setup: Parameterized Testing
+ ### Maven / Gradle Dependency
- ### Maven
+ JUnit 5 parameterized tests require `junit-jupiter` (includes params). Add `assertj-core` for assertions:
+
```xml
+ <!-- Maven -->
<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")
- }
+ // Gradle
+ testImplementation("org.junit.jupiter:junit-jupiter")
```
- ## Basic Pattern: `@`ValueSource
-
- ### Simple Value Testing
+ ### `@ValueSource` — Simple Values
```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();
- }
+ @ParameterizedTest
+ @ValueSource(strings = {"hello", "world", "test"})
+ void shouldCapitalizeAllStrings(String input) {
+ assertThat(StringUtils.capitalize(input)).isNotEmpty();
}
- ```
- ## `@`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
+ @ValueSource(ints = {1, 2, 3, 4, 5})
+ void shouldBePositive(int number) {
+ assertThat(number).isPositive();
+ }
- @ParameterizedTest
- @MethodSource("additionTestCases")
- void shouldAddNumbersCorrectly(int a, int b, int expected) {
- int result = Calculator.add(a, b);
- assertThat(result).isEqualTo(expected);
- }
+ @ParameterizedTest
+ @ValueSource(ints = {Integer.MIN_VALUE, -1, 0, 1, Integer.MAX_VALUE})
+ void shouldHandleBoundaryValues(int value) {
+ assertThat(Math.incrementExact(value)).isGreaterThan(value);
}
```
- ## `@`CsvSource for Tabular Data
-
- ### CSV-Based Test Data
+ ### `@CsvSource` — Tabular 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);
- }
+ @ParameterizedTest
+ @CsvSource({
+ "alice@example.com, true",
+ "bob@gmail.com, true",
+ "invalid-email, false",
+ "user@, false",
+ "@example.com, false"
+ })
+ void shouldValidateEmailAddresses(String email, boolean expected) {
+ assertThat(UserValidator.isValidEmail(email)).isEqualTo(expected);
}
```
- ## `@`CsvFileSource for External Data
-
- ### CSV File-Based Testing
+ ### `@MethodSource` — Complex Data
```java
import org.junit.jupiter.params.ParameterizedTest;
- import org.junit.jupiter.params.provider.CsvFileSource;
-
- class PriceCalculationTest {
+ import org.junit.jupiter.params.provider.MethodSource;
+ import java.util.stream.Stream;
- @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);
- }
+ @ParameterizedTest
+ @MethodSource("additionTestCases")
+ void shouldAddNumbersCorrectly(int a, int b, int expected) {
+ assertThat(Calculator.add(a, b)).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
+ static Stream<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)
+ );
+ }
```
- ## `@`EnumSource for Enum Testing
-
- ### Enum-Based Test Data
+ ### `@EnumSource` — Enum Values
```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(Status.class)
+ void shouldHandleAllStatuses(Status status) {
+ assertThat(status).isNotNull();
+ }
- @ParameterizedTest
- @EnumSource(value = Status.class, mode = EnumSource.Mode.EXCLUDE, names = {"DELETED"})
- void shouldHandleStatusesExcludingDeleted(Status status) {
- assertThat(status).isNotEqualTo(Status.DELETED);
- }
+ @ParameterizedTest
+ @EnumSource(value = Status.class, names = {"ACTIVE", "INACTIVE"})
+ void shouldHandleSpecificStatuses(Status status) {
+ assertThat(status).isIn(Status.ACTIVE, Status.INACTIVE);
}
```
- ## Custom Display Names
-
- ### Readable Test Output
+ ### Custom Display Names
```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);
- }
+ @ParameterizedTest(name = "Discount of {0}% should be calculated correctly")
+ @ValueSource(ints = {5, 10, 15, 20})
+ void shouldApplyDiscount(int discountPercent) {
+ double result = DiscountCalculator.apply(100.0, discountPercent);
+ assertThat(result).isEqualTo(100.0 * (1 - discountPercent / 100.0));
}
```
- ## Combining Multiple Sources
-
- ### ArgumentsProvider for Complex Scenarios
+ ### Custom `ArgumentsProvider`
```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 {
+ class RangeValidatorProvider 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
+ Arguments.of(0, 0, 100, true),
+ Arguments.of(50, 0, 100, true),
+ Arguments.of(-1, 0, 100, false),
+ Arguments.of(101, 0, 100, false)
);
}
}
- 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);
- }
+ @ParameterizedTest
+ @ArgumentsSource(RangeValidatorProvider.class)
+ void shouldValidateRange(int value, int min, int max, boolean expected) {
+ assertThat(RangeValidator.isInRange(value, min, max)).isEqualTo(expected);
}
```
- ## 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
+ ### Error Condition Testing
- **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
+ ## Best Practices
- **Parameter not matching**: Verify number and type of parameters match test method signature.
+ - Use descriptive display names: `name = "{0}..."` for readable output
+ - Test boundary values: include min, max, zero, and edge cases
+ - Keep test logic focused: single assertion per parameter set
+ - Use `@MethodSource` for complex objects, `@CsvSource` for tabular data
+ - Organize test data logically — group related scenarios together
- **Display name not showing**: Check parameter syntax in `name = "..."`.
+ ## Constraints and Warnings
- **CSV parsing error**: Ensure CSV format is correct and quote strings containing commas.
+ - **Parameter count must match**: Number of parameters from source must match test method signature
+ - **`@ValueSource` limitation**: Only supports primitives, strings, and enums — not objects or null directly
+ - **CSV escaping**: Strings with commas must use single quotes in `@CsvSource`
+ - **`@MethodSource` visibility**: Factory methods must be static in the same test class
+ - **Display name placeholders**: Use `{0}`, `{1}`, etc. to reference parameters
+ - **Execution count**: Each parameter set runs as a separate test invocation
## 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)
+ - [`@ParameterizedTest` API](https://junit.org/junit5/docs/current/api/org.junit.jupiter.params/org/junit/jupiter/params/ParameterizedTest.html)