CtrlK
BlogDocsLog inGet started
Tessl Logo

unit-test-parameterized

Parameterized testing patterns with @ParameterizedTest, @ValueSource, @CsvSource. Run single test method with multiple input combinations. Use when testing multiple scenarios with similar logic.

Install with Tessl CLI

npx tessl i github:giuseppe-trisciuoglio/developer-kit --skill unit-test-parameterized
What are skills?

82

Does it follow best practices?

Validation for skill structure

SKILL.md
Review
Evals

Parameterized Unit Tests with JUnit 5

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({
    "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

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
  • @ParameterizedTest Documentation
  • Boundary Value Analysis
Repository
giuseppe-trisciuoglio/developer-kit
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.