W3docs

Java JUnit Parameterized Tests

Run the same JUnit test with different inputs using @ParameterizedTest and value sources.

Java JUnit Parameterized Tests

A parameterized test runs the same test method many times, once for each set of inputs you feed it. Instead of copy-pasting testReverseAbc, testReverseEmpty, and testReverseSingle, you write the logic once and supply a data source — a list of inputs and expected results. JUnit 5 (the Jupiter engine) makes this first-class with @ParameterizedTest and a family of source annotations. The payoff is fewer lines, denser coverage, and each input reported as its own pass/fail.

From repeated tests to one parameterized test

A plain @Test method tests exactly one scenario. When you want to check the same behaviour across a table of inputs, the naive approach repeats the method:

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

class PrimesTest {
  @Test void two_isPrime()   { assertTrue(Primes.isPrime(2)); }
  @Test void seven_isPrime() { assertTrue(Primes.isPrime(7)); }
  @Test void thirteen_isPrime() { assertTrue(Primes.isPrime(13)); }
}

The parameterized version collapses all three into one method. You annotate with @ParameterizedTest (not @Test) and attach a source that supplies the argument for each run:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;

class PrimesTest {
  @ParameterizedTest
  @ValueSource(ints = {2, 7, 13})
  void isPrime(int candidate) {
    assertTrue(Primes.isPrime(candidate));
  }
}

JUnit invokes isPrime three times — candidate=2, then 7, then 13 — and reports three results. One failing value does not hide the others.

Choosing an argument source

The @ParameterizedTest annotation is useless on its own; it needs a source that produces the arguments. JUnit Jupiter ships several, each suited to a different shape of data:

SourceSuppliesBest for
@ValueSourceA single literal per run (ints, strings, doubles, …)One-argument tests
@CsvSourceA row of comma-separated values per runA few inline rows with multiple columns
@CsvFileSourceRows read from a classpath .csv fileLarge or externally maintained tables
@MethodSourceWhatever a factory method returns as a Stream/CollectionComplex objects, computed cases
@EnumSourceThe constants of an enumExhaustively covering an enum
@NullSource / @EmptySourcenull and empty valuesEdge-case coverage of strings/collections

The rule of thumb: @ValueSource for one simple input, @CsvSource for a small multi-column table, and @MethodSource once the data stops fitting in annotation literals.

Multiple columns with @CsvSource

When each case has an input and an expected output, @CsvSource gives you a tiny table inline. Each string is one row; commas split it into method parameters in order:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;

class StringsTest {
  @ParameterizedTest
  @CsvSource({
      "abc,     cba",
      "racecar, racecar",
      "'',      ''"          // single quotes denote an empty string
  })
  void reverse(String input, String expected) {
    assertEquals(expected, Strings.reverse(input));
  }
}

JUnit converts each comma-separated token to the declared parameter type, so @CsvSource({"4, 16"}) can land in (int n, int square). Use single quotes to include commas or empty strings inside a cell.

Computed cases with @MethodSource

Annotation values must be compile-time constants, so once arguments are real objects or need computing, switch to @MethodSource. It names a static method that returns a Stream<Arguments> (or any Collection/array):

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;

class TaxTest {
  static Stream<Arguments> brackets() {
    return Stream.of(
        Arguments.of(0,      0.0),
        Arguments.of(10_000, 1_000.0),
        Arguments.of(50_000, 7_500.0)
    );
  }

  @ParameterizedTest(name = "income {0} -> tax {1}")
  @MethodSource("brackets")
  void computesTax(int income, double expectedTax) {
    assertEquals(expectedTax, Tax.of(income));
  }
}

The optional name attribute customises how each invocation appears in the test report, with {0}, {1} standing in for the arguments — invaluable when a single failing row needs to be identified at a glance.

A worked example: a parameterized runner without JUnit

The code runner has no JUnit on its classpath, so this program models the mechanism a parameterized test embodies with plain JDK code: a single check is defined once, then driven over a list of cases — exactly what @ParameterizedTest does behind the annotations. One case is wrong on purpose so you can see how isolated rows pass or fail.

java— editable, runs on the server

What to take from the run:

  • The reverse block prints four PASS lines and >> reverse: 4 passed, 0 failed — one body (reverse) ran against four rows, mirroring how a single @ParameterizedTest method is invoked once per @CsvSource row.
  • The isPrime block prints PASS for inputs 2, 7, 9, and 1, but FAIL for input 4, because isPrime(4) returns false while the row claimed true — a wrong expectation, not a code bug, which is the most common parameterized-test mistake.
  • That single failure is reported on its own line and counted as >> isPrime: 4 passed, 1 failed; the other rows still pass, demonstrating the key advantage over a hand-rolled loop with one assertion — each input is an independent, individually reported case.
  • The runAll helper takes the unit as a Function and the cases as a List, separating the logic under test from the data — exactly the separation @ParameterizedTest plus an argument source gives you.
  • Every line shows expected next to actual, so the 4 / expected=true / actual=false row tells you precisely which value disagreed — the same diagnostic value JUnit's assertEquals message and the name = "..." template provide.

Practice

Practice

In JUnit 5, your test needs to run once for each row of a table where every row has both an input string and the expected reversed string. Which is the most appropriate single annotation to supply that data inline?