Files
gh-giuseppe-trisciuoglio-de…/skills/unit-test-parameterized/SKILL.md
2025-11-29 18:28:34 +08:00

10 KiB

name, description, category, tags, version
name description category tags version
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. testing
junit-5
parameterized-test
value-source
csv-source
method-source
1.0.1

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