DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Quality Assurance in AI-Driven Business Evolution
  • The New Testing Pattern: Standardizing Regression for Cloud Migrations
  • From Test Automation to Autonomous Quality: Designing AI Agents for Data Validation at Scale
  • Building Realistic Test Data in Java: A Hands-On Guide for Developers

Trending

  • Why AI-Generated Code Breaks Your Testing Assumptions
  • Retesting Best Practices for Agile Teams: A Quick Guide to Bug Fix Verification
  • Run Gemma 4 on Your Laptop: A Hands-On Guide to Google's Latest Open Multimodal LLM
  • S3 Vectors: How to Build a RAG Without a Vector Database
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. Introduction to Data-Driven Testing With JUnit 5: A Guide to Efficient and Scalable Testing

Introduction to Data-Driven Testing With JUnit 5: A Guide to Efficient and Scalable Testing

Use JUnit 5’s @ParameterizedTest with @EnumSource and @MethodSource to run tests with multiple data inputs, improve test coverage, and efficiency for robust applications.

By 
Otavio Santana user avatar
Otavio Santana
DZone Core CORE ·
Aug. 07, 25 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
3.2K Views

Join the DZone community and get the full member experience.

Join For Free

When discussing the history of software development, we can observe an increase in software complexity, characterized by more rules and conditions. When it comes to modern applications that rely heavily on databases, testing how the application interacts with its data becomes equally important. It is where data-driven testing plays a crucial role.

Data-driven testing helps increase software quality by enabling tests with multiple data sets, which means the same test runs multiple times with different data inputs. Automating these tests also ensures scalability and repeatability across your test suite, reducing human error, boosting productivity, saving time, and guaranteeing that the same mistake doesn't happen twice.

Modern applications often depend on databases to store and manipulate critical data; indeed, the data is the soul of any modern application. Thus, it's essential to validate that these operations function correctly across a range of scenarios. Writing traditional unit tests often falls short because they don't account for the variability of data that real-world applications encounter. This is where data-driven testing shines.

When we talk about data-driven tests, it gives you the capability to automate those tests with different inputs, including several cases, to check if your application keeps up. Exploring this approach ensures that your application handles data consistently and reliably, helping you avoid bugs that may only appear with specific data types, formats, or combinations of data.

Data-driven testing is a strategy where the same test is run multiple times with different sets of input data. Rather than writing separate test cases for each data variation, you use one test method and provide other data sets to test against. Exploring more of the data-driven testing goes beyond reducing redundancy in your test code and also improves test coverage by ensuring the system behaves as expected across all types of data.

Data-driven test flow

Data-driven test flow

In this article, we will explore this capability with Java and Jupiter.

Live Session: Implementing Data-Driven Testing With Jakarta NoSQL and Jakarta Data

In this section, we will walk through a live example using Java SE, Jakarta NoSQL, and Jakarta Data to demonstrate data-driven testing in action. For our example, we will build a simple hotel management system that tracks room status and integrates with Oracle NoSQL as the database.

Prerequisites

Before diving into the code, ensure you have Oracle NoSQL running either on the cloud or locally using Docker. You can quickly start Oracle NoSQL by running the following command:

Shell
 
docker run -d --name oracle-instance -p 8080:8080 ghcr.io/oracle/nosql:latest-ce


Once the database is up and running, we're ready to start building the project.

You can also find the full project on GitHub: Data-Driven Test with Oracle NoSQL

Step 1: Structure the Entity

We begin by defining the Room entity, which represents a hotel room in our system. This entity is mapped to the database using the @Entity annotation, and each field corresponds to a column in the database:

Java
 
@Entity
public class Room {

    @Id
    private String id;

    @Column
    private int number;

    @Column
    private RoomType type;

    @Column
    private RoomStatus status;

    @Column
    private CleanStatus cleanStatus;

    @Column
    private boolean smokingAllowed;

    @Column
    private boolean underMaintenance;
}


Step 2: Room Repository

Next, we create the RoomRepository interface, which uses Jakarta Data and NoSQL annotations to define queries for various room-related operations:

Java
 
@Repository
public interface RoomRepository {

    @Query("WHERE type = 'VIP_SUITE' AND status = 'AVAILABLE' AND underMaintenance = false")
    List<Room> findVipRoomsReadyForGuests();

    @Query("WHERE type <> 'VIP_SUITE' AND status = 'AVAILABLE' AND cleanStatus = 'CLEAN'")
    List<Room> findAvailableStandardRooms();

    @Query("WHERE cleanStatus <> 'CLEAN' AND status <> 'OUT_OF_SERVICE'")
    List<Room> findRoomsNeedingCleaning();

    @Query("WHERE smokingAllowed = true AND status = 'AVAILABLE'")
    List<Room> findAvailableSmokingRooms();

    @Save
    void save(List<Room> rooms);

    @Save
    Room newRoom(Room room);

    void deleteBy();

    @Query("WHERE type = :type")
    List<Room> findByType(@Param("type") String type);
}


In this repository, we define several queries to retrieve rooms based on different conditions, such as finding available rooms, rooms that need cleaning, or rooms that allow smoking. We also include methods for saving, deleting, and querying rooms by type.

To test our repository, we want to ensure that we are using a test container instead of a production environment. For this, we set up a DatabaseContainer singleton that starts the Oracle NoSQL container for testing purposes:

Java
 
public enum DatabaseContainer {

    INSTANCE;

    private final GenericContainer<?> container = new GenericContainer<>
            (DockerImageName.parse("ghcr.io/oracle/nosql:latest-ce"))
            .withExposedPorts(8080);

    {
        container.start();
    }

    public DatabaseManager get(String database) {
        DatabaseManagerFactory factory = managerFactory();
        return factory.apply(database);
    }

    public DatabaseManagerFactory managerFactory() {
        var configuration = DatabaseConfiguration.getConfiguration();
        Settings settings = Settings.builder()
                .put(OracleNoSQLConfigurations.HOST, host())
                .build();
        return configuration.apply(settings);
    }

    public String host() {
        return "http://" + container.getHost() + ":" + container.getFirstMappedPort();
    }
}


This container ensures that we’re using the Oracle NoSQL database, which is running inside a Docker container, thereby mimicking a production-like environment while remaining fully isolated for testing purposes.

Step 4: Injecting the DatabaseManager

We need to inject the DatabaseManager into our CDI context. For this, we create a ManagerSupplier class that ensures the DatabaseManager is available to our application:

Java
 
@ApplicationScoped
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
public class ManagerSupplier implements Supplier<DatabaseManager> {

    @Produces
    @Database(DatabaseType.DOCUMENT)
    @Default
    public DatabaseManager get() {
        return DatabaseContainer.INSTANCE.get("hotel");
    }
}


Step 5: Writing Data-Driven Tests With @ParameterizedTest in JUnit 5

In this step, we focus on how to write data-driven tests using JUnit 5's @ParameterizedTest annotation, and specifically dive into the types used in the RoomServiceTest. We’ll explore the @EnumSource and @MethodSource annotations, all of which help run the same test method multiple times with different sets of input data.

Let’s look at the types used in the RoomServiceTest class in detail:

Java
 
@ParameterizedTest(name = "should find rooms by type {0}")
@EnumSource(RoomType.class)
void shouldFindRoomByType(RoomType type) {
    List<Room> rooms = this.repository.findByType(type.name());
    SoftAssertions.assertSoftly(softly -> softly.assertThat(rooms).allMatch(room -> room.getType().equals(type)));
}


The @EnumSource(RoomType.class) annotation is used to automatically provide each enum constant from the RoomType enum to the test method. In this case, the RoomType enum contains values like VIP_SUITE, STANDARD, SUITE, etc.

This annotation causes the test method to run once for each value in the RoomType enum. Each time the test runs, the type parameter is assigned one of the enum values, and the test checks that all rooms returned by the repository match the RoomType provided.

This is especially useful when you want to run the same test logic for all possible values of an enum. It ensures that your code works consistently across all variants of the enum type, minimizing redundant test cases.

Java
 
@ParameterizedTest
@MethodSource("room")
void shouldSaveRoom(Room room) {
    Room updateRoom = this.repository.newRoom(room);

    SoftAssertions.assertSoftly(softly -> {
        softly.assertThat(updateRoom).isNotNull();
        softly.assertThat(updateRoom.getId()).isNotNull();
        softly.assertThat(updateRoom.getNumber()).isEqualTo(room.getNumber());
        softly.assertThat(updateRoom.getType()).isEqualTo(room.getType());
        softly.assertThat(updateRoom.getStatus()).isEqualTo(room.getStatus());
        softly.assertThat(updateRoom.getCleanStatus()).isEqualTo(room.getCleanStatus());
        softly.assertThat(updateRoom.isSmokingAllowed()).isEqualTo(room.isSmokingAllowed());
    });
}


The @MethodSource("room") annotation specifies that the test method should be run with data provided by the room() method. This method returns a stream of Arguments containing different Room objects.

The room() method generates random room data using Faker and assigns random values to room attributes like roomNumber, type, status, etc. These randomly generated rooms are passed to the test method one at a time.

The test checks that the room saved in the repository matches the original room’s attributes, ensuring that the save operation works as expected.

@MethodSource is a great choice when you need to provide complex or custom test data. In this case, we use random data generation to simulate different room configurations, ensuring our code can handle a wide range of inputs without redundancy.

Conclusion

In this article, we've explored the importance of data-driven testing and how to implement it effectively using JUnit 5 (Jupiter). We demonstrated how to leverage parameterized tests to run the same test multiple times with different inputs, making our testing process more efficient, comprehensive, and scalable. By using annotations like @EnumSource, @MethodSource, and @ArgumentsSource, we can easily pass multiple sets of data to our test methods, ensuring that our application works as expected across a wide range of input conditions.

We focused on @EnumSource iterating over enum constants and @MethodSource generating custom data for our tests. These tools, alongside JUnit 5’s rich variety of parameterized test sources, such as @ValueSource, @CsvSource, and @ArgumentsSource, give us the flexibility to design tests that cover a broader spectrum of data variations.

By incorporating these techniques, we ensure that our repository methods (and other components) are robust, adaptable, and thoroughly tested with diverse real-world data. This approach significantly improves software quality, reduces test code duplication, and accelerates the testing process.

Data-driven testing isn’t just about automating tests; it’s about making those tests more meaningful by accounting for the variety of real-world conditions your software might face. It’s a valuable strategy for building resilient applications, and with JUnit 5, the possibilities for enhancing test coverage are vast and customizable.

JUnit Data (computing) Testing

Opinions expressed by DZone contributors are their own.

Related

  • Quality Assurance in AI-Driven Business Evolution
  • The New Testing Pattern: Standardizing Regression for Cloud Migrations
  • From Test Automation to Autonomous Quality: Designing AI Agents for Data Validation at Scale
  • Building Realistic Test Data in Java: A Hands-On Guide for Developers

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook