unit-test-parameterized
Install this skill
npx skills add giuseppe-trisciuoglio/developer-kitWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
Parameterized unit tests transform standard test methods into iterative loops by injecting distinct input variables into a shared verification logic. By moving away from writing separate methods for every edge case, this approach keeps test suites concise and maintainable. JUnit 5 provides multiple providers to supply these values, allowing developers to map scalar primitives, enum constants, tabular CSV rows, or dynamic stream objects directly into test parameters. This technique significantly boosts code coverage because adding new test vectors becomes a matter of updating a data source rather than duplicating entire test structures. The integration with AssertJ provides expressive verification syntax, ensuring that each iteration remains readable and precise. Ultimately, this practice enforces high-quality standard testing through data-driven automation, reducing redundant boilerplate code while ensuring logic holds up across broad input variations.
When to Use This Skill
- •Verifying mathematical functions with extensive input ranges
- •Checking regex-based validation for diverse string formats
- •Testing state transition logic against multiple enum states
- •Validating boundary conditions for API request processing
- •Importing large sets of real-world sample data for regression testing
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- “Refactor these redundant tests into a parameterized JUnit 5 test
- “How do I run the same test with different inputs in Java?
- “Convert this test method to use @ParameterizedTest
- “Show me how to pass CSV data into my unit tests
- “How to test enum values efficiently in JUnit 5?
Pro Tips
- 💡Combine `@ParameterizedTest` with custom `ArgumentsProvider` for complex data structures or external data sources, enhancing flexibility beyond simple `ValueSource` or `CsvSource`.
- 💡Always provide a descriptive name for your parameterized tests using the `name` attribute (e.g., `{index}: {0} should be {1}`) to make test reports more readable and actionable.
- 💡When testing a large number of scenarios, consider grouping related test data into separate parameterized test methods to maintain clarity and focus for each specific behavior.
What this skill does
- •Execute identical test logic across multiple input variations
- •Streamline data-driven scenarios using tabular formats
- •Filter enum values for partial or specific test iterations
- •Read external CSV datasets for large-scale integration checks
- •Provide custom display naming for individual test cycles
When not to use it
- ✕When test methods have drastically different assertions or behavior
- ✕For simple tests that only require a single, trivial input
- ✕When the setup logic for each test case is unique and complex
Example workflow
- Identify a test method with significant code duplication across input variants
- Select the appropriate provider, such as @ValueSource or @CsvSource, based on data shape
- Replace the hardcoded test variables with method arguments
- Annotate the method with @ParameterizedTest and the chosen source
- Run the test suite to observe individual results for each provided data point
Prerequisites
- –JUnit 5 (Jupiter) library
- –AssertJ for fluent assertions
- –A standard Java testing project structure
Pitfalls & limitations
- !Mixing incompatible data types in sources can lead to runtime conversion errors
- !Over-parameterizing simple tests can obscure the intent of the failure
- !Relying on external CSV files can break tests if file paths or formatting change
- !Complex MethodSource implementations can become difficult to debug
FAQ
How it compares
Unlike manual test duplication where every scenario requires a new method block, parameterized testing centralizes the verification logic, making it easier to maintain and update the entire test set from a single location.
📄 Full skill instructions — original source: giuseppe-trisciuoglio/developer-kit
Write efficient parameterized unit tests that run the same test logic with multiple input values. Reduce test duplication and improve test coverage using @ParameterizedTest.
## When to Use This Skill
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
## Setup: Parameterized Testing
### 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
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.assertj:assertj-core")
}## Basic Pattern: @ValueSource
### Simple Value Testing
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
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
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
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
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
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
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
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
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**:
@ParameterizedTest
@ValueSource(strings = {"", " ", null})
void shouldThrowExceptionForInvalidInput(String input) {
assertThatThrownBy(() -> Parser.parse(input))
.isInstanceOf(IllegalArgumentException.class);
}**Testing multiple valid inputs**:
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 5, 8, 13})
void shouldBeInFibonacciSequence(int number) {
assertThat(FibonacciChecker.isFibonacci(number)).isTrue();
}## 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)
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/unit-test-parameterized/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/giuseppe-trisciuoglio/developer-kit/unit-test-parameterized/SKILL.md - Cursor:
~/.cursor/skills/giuseppe-trisciuoglio/developer-kit/unit-test-parameterized/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/giuseppe-trisciuoglio/developer-kit/unit-test-parameterized/SKILL.md
🚀 Install with CLI:npx skills add giuseppe-trisciuoglio/developer-kit
