JUnit 5 - How to Write Parameterized Tests

Last Updated : 5 May, 2026

JUnit 5 provides support for parameterized tests, allowing a single test method to run multiple times with different inputs. This helps reduce duplicate code and improves test coverage.

  • Reuse one test method for multiple inputs
  • Reduce code duplication
  • Improve test readability and coverage

Prerequisites

Before starting, you should be familiar with:

Steps to Create Parameterized Tests in JUnit 5

Step 1: Dependency Setup

JUnit 5 does not support parameterized tests by default, so you must add:

XML
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.9.0</version>
    <scope>test</scope>
</dependency>

The full example of the pom.xml file:

XML
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
  
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>parameterized-tests-example</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>parameterized-tests-example</name>
    <description>parameterized-tests-example</description>

    <properties>
        <java.version>11</java.version>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>5.9.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.0</version>
            </plugin>
        </plugins>
    </build>
</project>

Gradle users need to add this dependency to the test implementation dependency section. It can be done by adding the following code block to the build.gradle file:

Kotlin
testImplementation(
        'org.junit.jupiter:junit-jupiter-params:5.9.0'
)

Step 2: Create First Parameterized Test

Let’s create a simple service for our first parameterized test. We’ll use a phone validation service that takes a String

Example: Phone Validation Service

Java
import java.util.regex.Pattern;

public interface PhoneValidationService {
    boolean validatePhone(String phone);
}

public class TestPhoneValidationService implements PhoneValidationService {

    private final Pattern phoneRegex = Pattern.compile("^\\+?(?:[0-9] ?){6,14}[0-9]$");

    @Override
    public boolean validatePhone(String phone) {
        return phone != null && phoneRegex.matcher(phone).matches();
    }
  
}

Our phone validation method should return true if the input is not null and matches the regex; otherwise, it should return false. For testing, we use @Test for simple unit tests. For parameterized tests, we use @ParameterizedTest along with an argument source, which provides multiple inputs (phone numbers in this case).

Java
import com.example.phone.PhoneValidationService;
import com.example.phone.TestPhoneValidationService;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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

class ValueSourceExampleParameterizedTest {

    private final PhoneValidationService phoneValidationService = new TestPhoneValidationService();

    @ParameterizedTest
    @ValueSource(strings = {"555 555 55 55", "5555555555", "+15555555555"})
    void testProcessValidPhones(String phone) {
        assertTrue(phoneValidationService.validatePhone(phone));
    }

    @ParameterizedTest
    @ValueSource(strings = {"555", "@+15555555555", "test"})
    void testProcessInvalidPhones(String phone) {
        assertFalse(phoneValidationService.validatePhone(phone));
    }
  
}

We use @ValueSource to supply multiple inputs—valid phone numbers in one test and invalid ones in another. Each value runs as a separate test case. Unlike regular tests, the method includes a parameter (e.g., String phoneNumber) to receive each input. Running the test executes all cases with their respective inputs.

Execution results of the ValueSourceExampleParameterizedTest class
Execution results of the ValueSourceExampleParameterizedTest class

@ValueSource supports arrays of primitive types and strings (e.g., int, double, boolean, String, etc.), but only one type at a time. Because of this limitation, it’s suitable only for simple test cases and cannot handle multiple arguments. In this article, we focus on simple data types and argument sources. More argument sources are available in the org.junit.jupiter.params.provider package—let’s look at a few useful ones.

Step 3: Using @NullSource and @EmptySource

We often test with null and empty values to ensure proper handling and avoid NullPointerException. JUnit provides @NullSource and @EmptySource to supply such inputs in parameterized tests.

Java
import com.example.phone.PhoneValidationService;
import com.example.phone.TestPhoneValidationService;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EmptySource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;

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

class ValueNullAndEmptySourceExampleParameterizedTest {

    private final PhoneValidationService phoneValidationService = new TestPhoneValidationService();

    @ParameterizedTest
    @ValueSource(strings = {"555 555 55 55", "5555555555", "+15555555555"})
    void testProcessValidPhones(String phone) {
        assertTrue(phoneValidationService.validatePhone(phone));
    }

    @ParameterizedTest
    @NullSource
    @EmptySource
    @ValueSource(strings = {"555", "@+15555555555", "test"})
    void testProcessInvalidPhones(String phone) {
        assertFalse(phoneValidationService.validatePhone(phone));
    }
  
}

As you can see, we have simply added these additional annotations besides @ValueSource. This change will add two additional test cases (null and empty values) to our method for testing invalid phone arguments. This how the execution results will look like for our updated test class:

Execution results of the ValueNullAndEmptySourceExampleParameterizedTest class
Execution results of the ValueNullAndEmptySourceExampleParameterizedTest class

Also, we can replace  @NullSource and @EmptySource annotations with a single one @NullAndEmptySource, which combines these two for our convenience. Next, we will review one more useful argument source, which may be used for simple test cases.

Step 4: Using @EnumSource

Let's assume that we have a method that receives some enum value as a parameter and performs some operations based on the value of this parameter. For example, the method for sending messages through different channels:

Java
import java.util.UUID;

public interface MessageService {
    Message sendMessage(Message message, Channel channel);
}

public class TestMessageService implements MessageService {
    @Override
    public Message sendMessage(Message message, Channel channel) {
        // send a message based on the channel value, 
          // and return the message object with a generated id
        message.setId(UUID.randomUUID().toString());
        return message;
    }
}

The enum class with possible channel values and the Message class will look like this:

Java
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Message {
    private String id;
    private String message;
}

public enum Channel {
    SMS, 
    EMAIL, 
    WHATSAPP, 
    SLACK
}

It will make sense to test this method's behavior with all possible channel types. And with parameterized tests, we can do this with a single test method, instead of creating a separate one for each channel type. This will be the case for using our next possible arguments source — @EnumSource. Let's firstly look at the code example and then analyze it:

Java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

class EnumSourceExampleParameterizedTest {

    private final MessageService messageService = new TestMessageService();

    @ParameterizedTest
    @EnumSource(Channel.class)
    void testSendMessage(Channel channel) {
        Message message = messageService.sendMessage(createMessage(channel), channel);
        assertNotNull(message);
        assertNotNull(message.getId());
        assertFalse(message.getId().isEmpty());
    }

    private Message createMessage(Channel channel) {
        // create a message based on the channel value
        return Message.builder()
                .message(String.format("Test %s message", channel))
                .build();
    }
}

To make it more realistic, we initialize a MessageService and add a createMessage method that builds messages based on the channel type (using Lombok’s builder pattern). We replace @ValueSource with @EnumSource, passing the Channel enum so the test runs for each enum value; the rest remains the same.

Execution results of the EnumSourceExampleParameterizedTest class
Execution results of the EnumSourceExampleParameterizedTest class

As we can see, our test method was executed four times with every value from the Channel enum class. But let's imagine the case when a couple of our channels have different behavior, and we need to implement a separate test method for it. Would it be possible to use a parameterized test from this case? Definitely, please refer to the example below to see how it works:

Java
import com.example.message.Channel;
import com.example.message.Message;
import com.example.message.MessageService;
import com.example.message.TestMessageService;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

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

class EnumSourceExampleParameterizedTest {

    private final MessageService messageService = new TestMessageService();

    @ParameterizedTest
    @EnumSource(value = Channel.class, names = {"WHATSAPP", "SLACK"})
    void testSendMessage(Channel channel) {
        Message message = messageService.sendMessage(createMessage(channel), channel);
        assertNotNull(message);
        assertNotNull(message.getId());
        assertFalse(message.getId().isEmpty());
    }

    @ParameterizedTest
    @EnumSource(value = Channel.class, names = {"SMS", "EMAIL"})
    void testSendMessageThroughEmailAndSmsChannels(Channel channel) {
        Message message = messageService.sendMessage(createMessage(channel), channel);
        assertNotNull(message);
        assertNotNull(message.getId());
        assertFalse(message.getId().isEmpty());
        // check other custom behavior
    }

    private Message createMessage(Channel channel) {
        // create a message based on the channel value
        return Message.builder()
                .message(String.format("Test %s message", channel))
                .build();
    }
  
}

We split the test into two methods: one for WhatsApp and Slack, and another for SMS and Email. Using @EnumSource, we keep the value as the enum class and specify required constants via the names property for each test.

Execution results of the EnumSourceExampleParameterizedTest class with decomposed test methods
Execution results of the EnumSourceExampleParameterizedTest class with decomposed test methods

Step 5: Customize Test Names

Before finishing, let’s enhance test clarity by customizing test case names. Instead of default enum values, we can define descriptive messages using the name property in @ParameterizedTest. This makes results more readable and easier to understand.

Java
import com.example.message.Channel;
import com.example.message.Message;
import com.example.message.MessageService;
import com.example.message.TestMessageService;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

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

class EnumSourceExampleParameterizedTest {

    private final MessageService messageService = new TestMessageService();

    @ParameterizedTest(name = "[{index}] Send a message through the {0} channel")
    @EnumSource(value = Channel.class, names = {"WHATSAPP", "SLACK"})
    void testSendMessage(Channel channel) {
        Message message = messageService.sendMessage(createMessage(channel), channel);
        assertNotNull(message);
        assertNotNull(message.getId());
        assertFalse(message.getId().isEmpty());
    }

    @ParameterizedTest(name = "[{index}] Send a message through the {0} channel")
    @EnumSource(value = Channel.class, names = {"SMS", "EMAIL"})
    void testSendMessageThroughEmailAndSmsChannels(Channel channel) {
        Message message = messageService.sendMessage(createMessage(channel), channel);
        assertNotNull(message);
        assertNotNull(message.getId());
        assertFalse(message.getId().isEmpty());
        // check other custom behavior
    }

    private Message createMessage(Channel channel) {
        // create a message based on the channel value
        return Message.builder()
                .message(String.format("Test %s message", channel))
                .build();
    }
  
}

Let's review the text pattern from the name property of the @ParameterizedTest annotation step by step:

  1. All dynamic variables should be placed in curly brackets. Like {index}, for example.
  2. The {index} variable is the index of the test method run. Like '[1] SMS' and '[2] EMAIL'. 
  3. The {0} variable is the reference to the first method argument from our test method. It's the 'Channel' in our case. In case we would have more than one argument, we can reference the second one as {1}, the third one as {2}, and so on.

This how the execution results will look like after this change:

Execution results of the EnumSourceExampleParameterizedTest with customized names
Execution results of the EnumSourceExampleParameterizedTest with customized names

Such customizations may be helpful when we have complex test cases which aren't intuitively understandable.

Comment
Article Tags:

Explore