JUnit Parameterized Test with Example using @Parameters

โšก Smart Summary

Parameterized tests in JUnit run the same test method repeatedly with different input values, so one method covers many scenarios. The @RunWith and @Parameters annotations supply the data set for every iteration.

  • ๐Ÿ”˜ Data source: A static @Parameters method returns a Collection of arrays, and each array becomes one test iteration.
  • โ˜‘๏ธ Runner: @RunWith(Parameterized.class) replaces the default BlockJUnit4ClassRunner and rebuilds the class once per data row.
  • โœ… Constructor: One public constructor stores a single row of data in the instance fields the test method asserts against.
  • ๐Ÿงช Worked example: Five input rows drive the sum() test, and the JUnit view reports Runs 5/5 with zero failures.
  • ๐Ÿ› ๏ธ JUnit 5: @ParameterizedTest with @ValueSource, @CsvSource or @MethodSource removes both the runner and the constructor.
  • ๐Ÿ“Œ Pitfalls: A non-static @Parameters method, two public constructors, or a missing junit-jupiter-params dependency stops the run.

JUnit parameterized test using @RunWith and @Parameters annotations

What is a Parameterized Test in JUnit?

A parameterized test is a test that executes the same test method over and over again using different values. It helps developers save time when writing tests that differ only in their inputs and expected results.

Using a parameterized test, one can set up a test method that retrieves data from some data source. This makes it the simplest form of data-driven testing available inside JUnit itself, with no external library required.

Consider a simple test that sums different numbers. The code may look like this:

JUnit test method repeating three assertEquals calls for the sum method

The approach above leads to a lot of redundancy. Every new pair of numbers needs another assert statement inside the same method, and a failure on the first assertion hides every assertion that follows it.

A simpler approach is needed. Using a parameterized test you can add a single method that supplies ten data inputs, and your test will run ten times automatically.

Steps to Create a Parameterized JUnit Test

The following code shows an example of a parameterized test. It tests the sum() method of the Airthematic class, which is the spelling used throughout the sample project.

Step 1) Create a class. In this example, we are going to input two numbers by using the sum(int, int) method, which will return the sum of the given numbers.

Airthematic class declaring a public sum method that adds two int arguments

Step 2) Create a parameterized test class.

Test class header annotated with @RunWith(Parameterized.class) and four private fields

Code Explanation

  • Code Line 11: Annotate your test class using @RunWith(Parameterized.class).
  • Code Line 13: Declaring the variable ‘firstNumber’ as private and type as int.
  • Code Line 14: Declaring the variable ‘secondNumber’ as private and type as int.
  • Code Line 15: Declaring the variable ‘expectedResult’ as private and type as int.
  • Code Line 16: Declaring the variable ‘airthematic’ as private and type as Airthematic.

@RunWith(class_name.class): the @RunWith annotation is used to specify its runner class name. If we do not specify any type as a parameter, the runtime will choose BlockJUnit4ClassRunner by default.

This class is responsible for tests to run with a new test instance. It is responsible for invoking JUnit lifecycle methods such as setup (associate resources) and teardown (release resources), which are described in the JUnit test fixture tutorial.

To parameterize, you need to annotate the class using @RunWith and pass the required .class to be tested.

Step 3) Create a constructor that stores the test data. It stores 3 variables.

Parameterized test constructor assigning three int arguments to instance fields

Step 4) Create a static method that generates and returns test data.

Static input method annotated with @Parameterized.Parameters returning a two-dimensional Object array

Code Line 32,33: Creating a two-dimensional array (providing input parameters for addition). Using the asList method we convert the data into a List type, since the return type of the input method is a Collection.

Code Line 30: Using the @Parameters annotation to create a set of input data to run our test.

The static method identified by the @Parameters annotation returns a Collection where each entry in the Collection will be the input data for one iteration of the test. Consider the element {1,2,3}. Here:

  • firstNumber = 1
  • secondNumber = 2
  • expectedResult = 3

Here each array element will be passed to the constructor, one at a time, as the class is instantiated multiple times. The five arrays declared in the example therefore produce the following five runs:

Iteration firstNumber secondNumber expectedResult Console line
[0] 1 2 3 Sum of Numbers = : 3
[1] 11 22 33 Sum of Numbers = : 33
[2] 111 222 333 Sum of Numbers = : 333
[3] 10 9 19 Sum of Numbers = : 19
[4] 100 9 109 Sum of Numbers = : 109

Step 5) The complete code.

Complete AirthematicTest listing with imports, constructor, @Parameters method and @Test method

Code Explanation:

  • Code Line 25: Using the @Before annotation to set up the resources (Airthematic.class here). The @Before annotation is used here to run before each test case. It contains the precondition of the test.
  • Code Line 36: Using the @Test annotation to create our test.
  • Code Line 39: Creating an assert statement to check whether our sum is equivalent to what we expected.

Step 6) Create a test runner class to run the parameterized test:

TestRunner class passing AirthematicTest.class to JUnitCore.runClasses and printing failures

Code Explanation:

  • Code Line 8: Declaring the main method of the class Test which will run our JUnit test.
  • Code Line 9: Executing test cases using JUnitCore.runClasses, which takes the test class name as a parameter (in our example we are using AirthematicTest.class).
  • Code Line 11: Processing the result using a for loop and printing out the failed result.
  • Code Line 13: Printing out the successful result.

Output:

Here is the output, which shows a successful test with no failure trace, as given below. Note that the JUnit view lists one entry per data row rather than a single test:

Eclipse JUnit view reporting Runs 5/5 with 0 errors and 0 failures for the parameterized class

See the result on the console, which shows the addition of the two numbers:

Eclipse console printing one Sum of Numbers line for each of the five parameter rows

Parameterized Tests in JUnit 5 with @ParameterizedTest

The example above is written for JUnit 4. JUnit 5 (Jupiter) drops the runner model completely, so @RunWith(Parameterized.class), the data constructor and the instance fields all disappear. The JUnit 4 code shown above is not obsolete: it still runs unchanged on the JUnit Platform through the vintage engine. New tests, however, are normally written with @ParameterizedTest.

Two dependencies are required: junit-jupiter-api for the test annotations and junit-jupiter-params for the parameterized support. Without the second artifact the source annotations will not resolve at all.

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class AirthematicTest {

    // one row per iteration, no constructor and no runner
    @ParameterizedTest(name = "{0} + {1} = {2}")
    @CsvSource({"1, 2, 3", "11, 22, 33", "111, 222, 333", "10, 9, 19", "100, 9, 109"})
    void sumOfTwoNumbers(int firstNumber, int secondNumber, int expectedResult) {
        assertEquals(expectedResult, new Airthematic().sum(firstNumber, secondNumber));
    }
}

Jupiter offers several argument sources, and the right one depends on the shape of the data:

Source annotation Supplies Use it when
@ValueSource A single column of literals The test takes exactly one argument
@CsvSource Inline comma-separated rows Small tables of numbers and strings read cleanly in the file
@CsvFileSource Rows read from a CSV on the test classpath The data set is large or maintained outside the code
@MethodSource A static factory returning a Stream of Arguments Real objects, computed values or randomised data are needed
@EnumSource The constants of an enum Every enum value must be exercised

Two rules catch most beginners. A source annotation placed on a plain @Test method is silently ignored, so the method must carry @ParameterizedTest. And an unquoted empty value in @CsvSource is read as null, while a quoted empty value is read as an empty string.

The JUnit 4 annotations used in this article map onto Jupiter as follows: @RunWith(Parameterized.class) becomes @ParameterizedTest plus a source annotation, @Parameters becomes @MethodSource or @CsvSource, and @Before becomes @BeforeEach. The full list is covered in the JUnit annotations tutorial.

Advantages and Limitations of Parameterized Tests

Parameterization is not free. It removes duplication, but it also constrains how a test can be written, so it is worth knowing both sides before converting an existing suite.

Advantages

  • Less duplication: One method replaces a block of near-identical assert statements, as the first screenshot in this article shows.
  • Cheaper coverage: Adding an edge case costs one more data row instead of a whole new test case method.
  • Precise reporting: Each iteration is reported separately, so the JUnit view identifies exactly which row failed rather than one aggregated failure.
  • Centralised data: Inputs live in a single method and can later be moved to a CSV file or a factory without touching the assertions.

Limitations

  • One shape of assertion: Every row runs the same assertions, so a scenario that needs different checks still needs its own test method.
  • Class-level scope in JUnit 4: The runner parameterizes the whole class, so unrelated @Test methods in that class also run once per row.
  • Unreadable reports: Without a name template, failures appear as testAirthematicTest[3], which says nothing about the data that broke.
  • Bulky inline data: Large arrays crowd out the test logic; move them to @CsvFileSource or a @MethodSource factory instead.

Common Errors in JUnit Parameterized Tests

Most parameterized failures are initialisation errors raised before a single assertion executes. The table below lists the messages that appear most often and what triggers them.

Message Cause Fix
Test class should have exactly one public constructor The class declares no public constructor, or two of them Keep one public constructor whose parameters match the data columns
No public static parameters method on class The @Parameters method is not public static, or returns the wrong type Declare it as public static Collection and return Arrays.asList(…)
IllegalArgumentException: wrong number of arguments A row is wider or narrower than the constructor parameter list Make every array in the Collection the same width as the constructor
Configuration error: no arguments provider A Jupiter test carries @ParameterizedTest with no source annotation Add @ValueSource, @CsvSource, @CsvFileSource, @MethodSource or @EnumSource
The source annotation appears to do nothing The method is annotated @Test instead of @ParameterizedTest Replace @Test with @ParameterizedTest and import junit-jupiter-params

One further trap is shared state. Because JUnit builds a new instance per row, anything held in a static field survives every iteration, and a value written by row [0] can quietly change the outcome of row [4]. Keep per-row state in instance fields and reset shared resources in the @Before or @BeforeEach method. General guidance on isolating tests is covered in the unit testing tutorial.

FAQs

TestNG supplies rows through a @DataProvider method referenced per test method, so unrelated tests in the class are unaffected. JUnit 4 parameterizes the entire class through its runner. JUnit 5 closes that gap with per-method @ParameterizedTest.

Yes. JUnit 4 accepts @Parameters(name = “{index}: sum({0},{1})={2}”) and Jupiter accepts @ParameterizedTest(name = “…”). Placeholders are replaced at runtime, so a failure report names the offending row instead of showing a bare index.

Yes. JUnit 4 supports @Parameter(0) and @Parameter(1) on public non-static fields, and the class then relies on the default constructor. Combining field injection with a data constructor triggers the exactly-one-public-constructor error.

JUnit 4 needs only the junit artifact, because the Parameterized runner ships inside it. JUnit 5 needs junit-jupiter-params alongside junit-jupiter-api; without that artifact @ParameterizedTest and every source annotation fail to resolve.

Jupiter provides @CsvFileSource(resources = “/data.csv”, numLinesToSkip = 1), which reads rows from the test classpath. JUnit 4 has no built-in equivalent, so the @Parameters method must open and parse the file itself before returning the Collection.

In JUnit 4 it can, but the runner parameterizes the whole class, so every method runs once per data row. Jupiter parameterizes individual methods, so plain @Test methods in the same class still execute exactly once.

AI assistants read a method signature and propose boundary rows such as zero, negative, maximum and overflow inputs that a hand-written table often misses. Review every generated expected result, because a model can produce a plausible row with the wrong answer.

GitHub Copilot produces the scaffold quickly but often mixes JUnit 4 and Jupiter imports, and sometimes leaves a source annotation on a plain @Test method. Check the imports before running the suite.

Summarize this post with: