skip to content

How does JUnit 4's `Parameterized` runner work — where does the data come from, how does each data row reach the test instance, and how many instances of the test class are created?

level: middleimportance: must knowfreq 40%

answer

  1. @RunWith(Parameterized.class)
  2. public static @Parameters → Iterable<Object[]> / Object[][]
  3. constructor injection OR public @Parameter(i) fields
  4. cross-product: methods × rows, fresh instance each
  5. name = "{index}: {0} + {1} = {2}"

basics

~20 s

Annotate the class @RunWith(Parameterized.class) and add a public static method annotated @Parameters returning Iterable<Object[]> or Object[][]. Each row is injected through a matching constructor, or into public fields annotated @Parameter(index). One instance is created per row per test method.

solid answer

~50 s

```java @RunWith(Parameterized.class) public class AdditionTest { @Parameters(name = "{index}: {0} + {1} = {2}") public static Iterable<Object[]> data() { … } } ``` The `@Parameters` method must be **public static**, is called once before the class runs, and returns the data rows. Each row reaches the test in one of two ways: a constructor whose parameters match the row, or **public** fields annotated `@Parameter(0)`, `@Parameter(1)`, … with a default constructor. The runner creates the **cross-product of test methods and data rows** — every `@Test` method runs once per row, on a fresh instance built from that row. The `name` attribute controls the display name via placeholders `{index}`, `{0}`, `{1}`, defaulting to the index alone. Extras worth knowing: `@Parameterized.BeforeParam` / `@AfterParam` (public static methods run before/after all tests for one parameter set, JUnit 4.13), and `@Parameterized.UseParametersRunnerFactory` to supply a custom per-parameter child runner.

code

java · 20 lines
java
@RunWith(Parameterized.class)
public class SlugifierTest {

    @Parameters(name = "{index}: slugify({0}) = {1}")
    public static Object[][] data() {
        return new Object[][] {
                { "Hello World", "hello-world" },
                { "  padded  ",  "padded" },
                { "a//b",        "a-b" }
        };
    }

    @Parameter(0) public String input;
    @Parameter(1) public String expected;

    @Test
    public void slugifies() {
        assertEquals(expected, Slugifier.slugify(input));
    }
}

go deeper

for a junior

Recall the annotations — @RunWith(Parameterized.class), public static @Parameters, constructor or @Parameter fields — and that every test runs once per row.

for a middle

Explain the cross-product model, both injection styles, the name template, and when the @Parameters method runs.

for a senior

Add @BeforeParam/@AfterParam, UseParametersRunnerFactory, the runtime cost of the cross-product, and diagnostics for the common initialization errors.

for a principal

Judge when a data table beats separate named tests, and how parameterized classes affect suite runtime, report readability and failure triage at scale.

## The shape of a parameterized test ```java @RunWith(Parameterized.class) public class AdditionTest { @Parameters(name = "{index}: {0} + {1} = {2}") public static Iterable<Object[]> data() { return Arrays.asList(new Object[][] { { 0, 0, 0 }, { 1, 1, 2 }, { 3, 2, 5 } }); } private final int a, b, sum; public AdditionTest(int a, int b, int sum) { this.a = a; this.b = b; this.sum = sum; } @Test public void adds() { assertEquals(sum, a + b); } } ``` ## Where the data comes from One method annotated `@Parameters`, which must be **public and static**. It may return `Iterable<Object[]>`, an `Object[][]`, or — when there is exactly one parameter — an `Iterable` or array of plain objects. It is invoked once, before any test runs; an exception thrown there is an initialization error and the whole class fails. Because it is static it cannot use per-instance state, and because it runs early it should not depend on setup performed by `@Before`. ## How a row reaches the instance Two injection styles: 1. **Constructor injection.** The runner picks the constructor and passes the row's elements positionally. Types must be assignment-compatible; a mismatch is an initialization error naming the parameter count or type. 2. **Field injection.** Declare a default constructor and mark **public** fields with `@Parameter(0)`, `@Parameter(1)`, …. The index defaults to 0, so a single-parameter test can simply write `@Parameter`. Field injection avoids long constructors but loses `final`. ## How many instances One test-class instance per (data row × test method) pair — the runner documents its model as the *cross-product* of test methods and data elements. Internally it builds one child runner per parameter set, and that child runner behaves like the default runner, creating a fresh instance per method. So a class with 3 methods and 5 rows executes 15 tests, and no state leaks between rows. ## Naming `@Parameters(name = "…")` accepts placeholders resolved by `MessageFormat`: `{index}` for the row number and `{0}`, `{1}`, … for parameter values. Without it the display name is just the index, which makes a failing report nearly unreadable — "test[3]" tells you nothing. Always set a name. Watch out for values whose `toString()` is enormous or contains characters your report format dislikes; braces in a value also need care because `MessageFormat` interprets them. ## Per-parameter fixtures JUnit 4.13 added `@Parameterized.BeforeParam` and `@Parameterized.AfterParam`: public static void methods that run once before and once after all tests for a given parameter set. They may take no arguments or the full parameter list. This fills the gap between `@BeforeClass` (once for the whole class) and `@Before` (every test). ## Customising the child runner `@Parameterized.UseParametersRunnerFactory(MyFactory.class)` replaces the factory that produces the per-parameter runner (the default produces a `BlockJUnit4ClassRunnerWithParameters`). This is the supported way for another framework to participate in a parameterized class despite the one-runner-per-class rule — the framework ships a factory rather than a runner. ## Common problems - **"No public static parameters method on class X"** — the method is missing, non-public, non-static, or the annotation is misspelled. - **Constructor mismatch** — the row's arity or types do not match; count the columns. - **Field injection silently empty** — the `@Parameter` fields must be public; a private field cannot be set. - **Mutable shared data** — rows returned from a static structure that tests mutate will bleed across rows; build fresh values in `data()`. - **Unreadable reports** — no `name` attribute. ## When to use it Parameterization is right when the *same* assertions apply to many inputs — boundary tables, format parsers, calculation matrices. It is wrong when each row needs a slightly different assertion, which is when people start branching inside the test on the parameter value: that is two tests wearing one coat.

  • If a class has 4 `@Test` methods and the `@Parameters` method returns 5 rows, how many tests execute?
    Twenty. The runner builds the cross-product of test methods and data rows, creating a child runner per parameter set and a fresh test-class instance per method within it. That is also why parameterized classes multiply runtime quickly — a heavy `@Before` is paid once per row per method.
  • What are the two ways a parameter row reaches the test instance, and when would you choose each?
    A constructor whose parameters match the row positionally, or public fields annotated `@Parameter(index)` with a default constructor. Constructors keep fields `final` and make the arity explicit, which suits two or three parameters. Field injection avoids a long, unreadable constructor when there are many columns, at the cost of non-final fields and the requirement that the fields be public.

saying these in an interview costs you the question

  • Declaring the `@Parameters` method non-static or non-public — it must be `public static`.
  • Using private fields with `@Parameter`; the runner cannot set them.
  • Believing one instance is reused for all rows; each row gets fresh instances.
  • Omitting the `name` attribute and then complaining that failure reports say only `test[3]`.
  • Branching inside the test body on the parameter value, which means the rows are not really the same test.

context