Skip to main content

Parameterized tests

Instead of writing multiple similar test methods that differ only in input values and expected results, use @ParameterizedTest to run the same test logic with different parameters. This reduces code duplication and makes it easier to add more test cases.

Avoid duplicate test methods

When testing the same logic with different inputs, writing separate test methods creates unnecessary duplication and makes maintenance harder.

StringUtilsTest.java
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;

class StringUtilsTest {

@Test
void emptyStringIsBlank() {
assertTrue(StringUtils.isBlank(""));
}

@Test
void whitespaceStringIsBlank() {
assertTrue(StringUtils.isBlank(" "));
}

@Test
void tabStringIsBlank() {
assertTrue(StringUtils.isBlank("\t"));
}

@Test
void nullStringIsBlank() {
assertTrue(StringUtils.isBlank(null));
}

@Test
void nonBlankStringIsNotBlank() {
assertFalse(StringUtils.isBlank("hello"));
}

@Test
void stringWithTextIsNotBlank() {
assertFalse(StringUtils.isBlank(" hello "));
}
}
warning

Multiple test methods with similar logic create unnecessary duplication and make maintenance harder. Adding a new test case requires copying an entire method.

Multiple parameter sources

JUnit 5 and 6 provide various parameter sources for different use cases.

MathUtilsTest.java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.assertThat;

class MathUtilsTest {

@ParameterizedTest
@ValueSource(ints = {2, 4, 6, 8, 10})
void evenNumbers(int number) {
assertThat(number % 2).isEqualTo(0);
}

@ParameterizedTest
@ValueSource(strings = {"radar", "level", "noon"})
void palindromes(String word) {
assertThat(isPalindrome(word)).isTrue();
}
}
info

@ValueSource is the simplest parameter source, supporting primitive types and strings.

Custom display names

You can customize how each parameterized test is displayed in test reports.

StringTest.java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.assertThat;

class StringTest {

@ParameterizedTest
@ValueSource(strings = {"", " ", "\t"})
void blankStrings(String input) {
assertThat(input.isBlank()).isTrue();
}
}
info

By default, test names include the parameter index: blankStrings(String) [1], blankStrings(String) [2], etc.

Parameterized Classes (JUnit 6)

When you have multiple test methods that all need to operate on the same object instance with different configurations, duplicating test logic becomes tedious. While @ParameterizedTest is great for testing a single method with different inputs, @ParameterizedClass takes parameterization to the class level, running all test methods for each parameter set.

Understanding the use case

Imagine you're testing various aspects of a ShoppingCart object—checking if it calculates totals correctly, applies discounts properly, handles taxes, etc. Each test method currently creates the same cart configuration. To test with different cart scenarios, you face two unappealing options:

  1. Duplicate the entire test class for each cart scenario you want to test
  2. Convert every test method to use @ParameterizedTest, adding complexity to each method

@ParameterizedClass solves this by parameterizing the entire class: you write your test suite once, and it automatically runs against multiple object instances. Think of it as "@ParameterizedTest for an entire class."

How it works

With @ParameterizedClass:

  • The parameterization annotations are placed on the test class (not individual methods)
  • All @Test methods run once for each parameter set provided
  • Parameters can be injected via constructor or fields annotated with @Parameter
  • Each parameter set gets its own fresh test class instance
  • Perfect for testing the same object behavior with different configurations
ShoppingCartTest.java
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.assertj.core.api.Assertions.assertThat;

class ShoppingCartTest {

@Test
void cartIsNotNull() {
ShoppingCart cart = new ShoppingCart(new BigDecimal("100.00"), new BigDecimal("0.10"));
assertThat(cart).isNotNull();
}

@Test
void cartCalculatesSubtotal() {
ShoppingCart cart = new ShoppingCart(new BigDecimal("100.00"), new BigDecimal("0.10"));
assertThat(cart.getSubtotal()).isEqualByComparingTo(new BigDecimal("100.00"));
}

@Test
void cartAppliesDiscount() {
ShoppingCart cart = new ShoppingCart(new BigDecimal("100.00"), new BigDecimal("0.10"));
assertThat(cart.getDiscount()).isEqualByComparingTo(new BigDecimal("10.00"));
}

@Test
void cartCalculatesTotal() {
ShoppingCart cart = new ShoppingCart(new BigDecimal("100.00"), new BigDecimal("0.10"));
assertThat(cart.getTotal()).isEqualByComparingTo(new BigDecimal("90.00"));
}
}
warning

Every test method creates the same ShoppingCart instance. To test multiple cart scenarios, you'd need to duplicate the entire test class or use @ParameterizedTest on each method individually.

Constructor vs Field Injection

@ParameterizedClass supports two injection styles:

CalculatorTest.java
@ParameterizedClass
@CsvSource({
"5, 3, 8, 2",
"10, 4, 14, 6",
"100, 25, 125, 75"
})
class CalculatorTest {

private final Calculator calculator;
private final int expectedSum;
private final int expectedDifference;

// Parameters injected via constructor
CalculatorTest(int a, int b, int expectedSum, int expectedDifference) {
this.calculator = new Calculator(a, b);
this.expectedSum = expectedSum;
this.expectedDifference = expectedDifference;
}

@Test
void testAddition() {
assertThat(calculator.add()).isEqualTo(expectedSum);
}

@Test
void testSubtraction() {
assertThat(calculator.subtract()).isEqualTo(expectedDifference);
}
}
info

Constructor injection is preferred when you want to initialize objects or perform setup with the parameters.

Using @MethodSource with Complex Objects

You can also use @MethodSource to provide complex objects directly:

UserValidatorTest.java
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.junit.jupiter.params.provider.Arguments.argumentSet;

@ParameterizedClass
@MethodSource("users")
class UserValidatorTest {

private final User user;
private final boolean expectedValid;

UserValidatorTest(User user, boolean expectedValid) {
this.user = user;
this.expectedValid = expectedValid;
}

static Stream<Arguments> users() {
return Stream.of(
argumentSet("valid user", new User("john@example.com", 25), true),
argumentSet("invalid email", new User("not-an-email", 25), false),
argumentSet("too young", new User("jane@example.com", 15), false)
);
}

@Test
void testUserValidation() {
assertThat(UserValidator.isValid(user)).isEqualTo(expectedValid);
}

@Test
void testUserIsNotNull() {
assertThat(user).isNotNull();
}
}
tip

Use @MethodSource when:

  • You need to pass complex objects to the test class
  • You want to reuse the same parameter provider across multiple test classes
  • You need to perform complex setup logic for your test data

When to use @ParameterizedClass vs @ParameterizedTest

Feature@ParameterizedClass@ParameterizedTest
ScopeEntire test classIndividual test method
Use caseMultiple tests on the same objectSingle test with different inputs
Test countAll methods × parameters1 method × parameters
SetupOnce per parameter setN/A
Best forTesting object behavior comprehensivelyTesting method logic with various inputs
info

Choose @ParameterizedClass when:

  • You have a suite of related tests that should all run against multiple instances
  • You want to test the same object from different angles
  • You need to share setup logic across multiple test methods

Choose @ParameterizedTest when:

  • You're testing a single method with different inputs
  • Each test is independent and doesn't share state
  • You want to parameterize only specific test methods