Otavio Santana, Author at foojay https://foojay.io/today/author/otavio-santana/ a place for friends of OpenJDK Tue, 27 Jan 2026 16:35:55 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://foojay.io/wp-content/uploads/2020/04/Favicon-3-2-150x150.png Otavio Santana, Author at foojay https://foojay.io/today/author/otavio-santana/ 32 32 Introduction to Behavior Driving Development with Java and MongoDB https://foojay.io/today/introduction-to-behavior-driving-development-with-java-and-mongodb/ https://foojay.io/today/introduction-to-behavior-driving-development-with-java-and-mongodb/#respond Tue, 27 Jan 2026 16:35:53 +0000 https://foojay.io/?p=122177 Table of Contents PrerequisitesStep 1: Create the project structureStep 2: Create the test infrastructureStep 3: Generate our first scenario testConclusion When we face software development, the biggest mistake is about delivering what the client wants. It sounds like a cliché, ...

The post Introduction to Behavior Driving Development with Java and MongoDB appeared first on foojay.

]]>
Table of Contents
PrerequisitesStep 1: Create the project structureStep 2: Create the test infrastructureStep 3: Generate our first scenario testConclusion

When we face software development, the biggest mistake is about delivering what the client wants. It sounds like a cliché, but after decades, we are still facing this problem. One good way to solve it is to start the test focusing on what the business needs.

Behavior-driven development (BDD) is a software development methodology where the focus is on behavior and the domain terminology or ubiquitous language. It utilizes a shared, natural language to define and test software behaviors from the user's perspective. BDD builds upon test-driven development (TDD) by focusing on scenarios that are relevant to the business. These scenarios are written as plain-language specifications that can be automated as tests, simultaneously serving as living documentation.

This approach fosters a common understanding among both technical and non-technical stakeholders, ensures that the software meets user needs, and helps reduce rework and development time. In this article, we will explore more about this approach and how to use it with MongoDB and Java.

In this tutorial, you’ll:

  • Model a domain (Room, RoomType, RoomStatus).
  • Write semantic repository queries using Jakarta Data.
  • Run data-driven tests using JUnit 5 and AssertJ.
  • Validate MongoDB queries in isolation using Testcontainers and Weld.

You can find all the code presented in this tutorial in the GitHub repository:

git clone git@github.com:soujava/behavior-driven-development-mongodb.git

Prerequisites

For this tutorial, you’ll need:

  • Java 21.
  • Maven.
  • A MongoDB cluster.

You can use the following Docker command to start a standalone MongoDB instance:

docker run --rm -d --name mongodb-instance -p 27017:27017 mongo

In this tutorial, we’ll use a Java SE project—without any heavyweight frameworks—to demonstrate how to combine Jakarta Data, JNoSQL, and JUnit 5 to write expressive, testable queries against MongoDB. Our focus will be on clarity, maintainability, and aligning tests with the business language, not just with database fields.

Step 1: Create the project structure

The first step is generating the project using Maven. To make it easier, we have the Maven Archetype. Thus, generate the following command:

mvn archetype:generate                     \

"-DarchetypeGroupId=io.cucumber"           \

"-DarchetypeArtifactId=cucumber-archetype" \

"-DarchetypeVersion=7.30.0"                \

"-DgroupId=org.soujava.demos.mongodb"      \

"-DartifactId=behavior-driven-development" \

"-Dpackage=org.soujava.demos.mongodb"      \

"-Dversion=1.0.0-SNAPSHOT"                 \

"-DinteractiveMode=false"

The next step is to include Eclipse JNoSQL with MongoDB, the Jakarta EE components implementations: CDI, JSON, and the Eclipse Microprofile implementation. 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.soujava.demos.mongodb</groupId>
    <artifactId>behavior-driven-development</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <jnosql.version>1.1.10</jnosql.version>
        <weld.se.core.version>6.0.3.Final</weld.se.core.version>
        <mockito.verson>5.18.0</mockito.verson>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.cucumber</groupId>
                <artifactId>cucumber-bom</artifactId>
                <version>7.30.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>5.14.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.assertj</groupId>
                <artifactId>assertj-bom</artifactId>
                <version>3.27.6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.jboss.weld.se</groupId>
            <artifactId>weld-se-shaded</artifactId>
            <version>${weld.se.core.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse</groupId>
            <artifactId>yasson</artifactId>
            <version>3.0.4</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>io.smallrye.config</groupId>
            <artifactId>smallrye-config-core</artifactId>
            <version>3.13.3</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jnosql.databases</groupId>
            <artifactId>jnosql-mongodb</artifactId>
            <version>${jnosql.version}</version>
        </dependency>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-java</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.cucumber</groupId>
            <artifactId>cucumber-junit-platform-engine</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-suite</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>${mockito.verson}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>${mockito.verson}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>mongodb</artifactId>
            <version>1.21.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.14.1</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.4</version>
                <configuration>
                    <properties>
                        <!-- Work around. Surefire does not include enough
                             information to disambiguate between different
                             examples and scenarios. -->
                        <configurationParameters>
                            cucumber.junit-platform.naming-strategy=long
                        </configurationParameters>
                    </properties>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

To simplify the scope of the tutorial, we will reuse the modeling and entity from the previous post about data-driven testing with MongoDB and Java. Thus, we will use the same hotel management at the org.soujava.demos.mongodb.document package:

package org.soujava.demos.mongodb.document;
public enum CleanStatus {
    CLEAN,
    DIRTY,
    INSPECTION_NEEDED
}

package org.soujava.demos.mongodb.document;
public enum RoomStatus {
    AVAILABLE,
    RESERVED,
    UNDER_MAINTENANCE,
    OUT_OF_SERVICE
}

package org.soujava.demos.mongodb.document;
public enum RoomType {
    STANDARD,
    DELUXE,
    SUITE,
    VIP_SUITE
}

package org.soujava.demos.mongodb.document;
import jakarta.nosql.Column;
import jakarta.nosql.Convert;
import jakarta.nosql.Entity;
import jakarta.nosql.Id;
import org.eclipse.jnosql.databases.mongodb.mapping.ObjectIdConverter;
import java.util.Objects;
@Entity
public class Room {
    @Id
    @Convert(ObjectIdConverter.class)
    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;
    public Room() {
    }

    public Room(String id, int number,
                RoomType type, RoomStatus status,
                CleanStatus cleanStatus,
                boolean smokingAllowed, boolean underMaintenance) {

        this.id = id;
        this.number = number;
        this.type = type;
        this.status = status;
        this.cleanStatus = cleanStatus;
        this.smokingAllowed = smokingAllowed;
        this.underMaintenance = underMaintenance;
    }

    public String getId() {
        return id;
    }

    public int getNumber() {
        return number;
    }

    public RoomType getType() {
        return type;
    }

    public RoomStatus getStatus() {
        return status;
    }

    public CleanStatus getCleanStatus() {
        return cleanStatus;
    }

    public boolean isSmokingAllowed() {
        return smokingAllowed;
    }

    public boolean isUnderMaintenance() {
        return underMaintenance;
    }

    public void update(RoomStatus newStatus) {
        this.status = newStatus;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Room room = (Room) o;
        return Objects.equals(id, room.id);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id);
    }

    @Override
    public String toString() {
        return "Room{" +
                "id='" + id + '\'' +
                ", roomNumber=" + number +
                ", type=" + type +
                ", status=" + status +
                ", cleanStatus=" + cleanStatus +
                ", smokingAllowed=" + smokingAllowed +
                ", underMaintenance=" + underMaintenance +
                '}';
    }

    public static RoomBuilder builder() {
        return new RoomBuilder();
    }
}

package org.soujava.demos.mongodb.document;

public class RoomBuilder {
    private String id;
    private int roomNumber;
    private RoomType type;
    private RoomStatus status;
    private CleanStatus cleanStatus;
    private boolean smokingAllowed;
    private boolean underMaintenance;

    public RoomBuilder id(String id) {
        this.id = id;
        return this;
    }

    public RoomBuilder number(int roomNumber) {
        this.roomNumber = roomNumber;
        return this;
    }

    public RoomBuilder type(RoomType type) {
        this.type = type;
        return this;
    }

    public RoomBuilder status(RoomStatus status) {
        this.status = status;
        return this;
    }

    public RoomBuilder cleanStatus(CleanStatus cleanStatus) {
        this.cleanStatus = cleanStatus;
        return this;
    }

    public RoomBuilder smokingAllowed(boolean smokingAllowed) {
        this.smokingAllowed = smokingAllowed;
        return this;
    }

    public RoomBuilder underMaintenance(boolean underMaintenance) {
        this.underMaintenance = underMaintenance;
        return this;
    }

    public Room build() {
        return new Room(id, roomNumber, type, status, cleanStatus, smokingAllowed, underMaintenance);
    }
}

The next step is to create an interface of communication between MongoDB and Java. We will simplify our lives using Jakarta Data. Thus, we will have a single interface, where we will connect to MongoDB as a repository interface, and the Jakarta provider will handle the implementation.

package org.soujava.demos.mongodb.document;

import jakarta.data.repository.Query;
import jakarta.data.repository.Repository;
import jakarta.data.repository.Save;
import java.util.List;
import java.util.Optional;

@Repository
public interface RoomRepository {
    @Query("FROM Room")
    List<Room> findAll();
    @Save
    Room save(Room room);
    void deleteBy();
    Optional<Room> findByNumber(Integer number);
}

We will enable CDI and the proper files, thus generating the bean.xml and the configuration properties file at the src/main/resources/META-INF.

beans.xml

<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee

http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"

       bean-discovery-mode="all">

</beans>

microprofile-config.properties

jnosql.mongodb.url=mongodb://localhost:27017

# mandatory define the database name

jnosql.document.database=hotels

Exploring the methodology, we should start with the behavior and then do the implementation. Therefore, at the test, we will generate our first feature file at the resource. We will create a room.feature at the src/test/resources/org/soujava/demos/mongodb folder.

Feature: Manage hotel rooms

  Scenario: Register a new room
    Given the hotel management system is operational
    When I register a room with number 203
    Then the room with number 203 should appear in the room list

  Scenario: Register multiple rooms
    Given the hotel management system is operational
    When I register the following rooms:
      | number | type      | status             | cleanStatus |
      | 101    | STANDARD  | AVAILABLE          | CLEAN       |
      | 102    | SUITE     | RESERVED           | DIRTY       |
      | 103    | VIP_SUITE | UNDER_MAINTENANCE  | CLEAN       |
    Then there should be 3 rooms available in the system

  Scenario: Change room status
    Given the hotel management system is operational
    And a room with number 101 is registered as AVAILABLE
    When I mark the room 101 as OUT_OF_SERVICE
    Then the room 101 should be marked as OUT_OF_SERVICE

Step 2: Create the test infrastructure

As we will need to generate a MongoDB instance for the test, we will use a container and run the test on it. We will create a DatabaseContainer as a singlethon instance. At the src/test/java/org/soujava/demos/mongodb/config, make the class DatabaseContainer.

package org.soujava.demos.mongodb.config;

import org.eclipse.jnosql.communication.Settings;
import org.eclipse.jnosql.databases.mongodb.communication.MongoDBDocumentConfiguration;
import org.eclipse.jnosql.databases.mongodb.communication.MongoDBDocumentConfigurations;
import org.eclipse.jnosql.databases.mongodb.communication.MongoDBDocumentManager;
import org.eclipse.jnosql.databases.mongodb.communication.MongoDBDocumentManagerFactory;
import org.eclipse.jnosql.mapping.core.config.MappingConfigurations;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import java.util.HashMap;
import java.util.Map;

public enum DatabaseContainer {
    INSTANCE;
    private final GenericContainer<?> mongodb =
            new GenericContainer<>("mongo:latest")
                    .withExposedPorts(27017)
                    .waitingFor(Wait.defaultWaitStrategy());
    {
        mongodb.start();
    }

    public MongoDBDocumentManager get(String database) {
        Settings settings = getSettings(database);
        MongoDBDocumentConfiguration configuration = new MongoDBDocumentConfiguration();
        MongoDBDocumentManagerFactory factory = configuration.apply(settings);
        return factory.apply(database);
    }

    private Settings getSettings(String database) {
        Map<String,Object> settings = new HashMap<>();
        settings.put(MongoDBDocumentConfigurations.HOST.get()+".1", host());
        settings.put(MappingConfigurations.DOCUMENT_DATABASE.get(), database);
        return Settings.of(settings);
    }

    public String host() {
        return mongodb.getHost() + ":" + mongodb.getFirstMappedPort();
    }
}

The next step is making this database available to the CDI container. We will create a ManagerSupplier that teaches the CDI how to generate a MongoDB instance. In this case, we will use the properties from the MongoDB test container.

import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.Typed;
import jakarta.interceptor.Interceptor;
import org.eclipse.jnosql.communication.semistructured.DatabaseManager;
import org.eclipse.jnosql.databases.mongodb.communication.MongoDBDocumentManager;
import org.eclipse.jnosql.mapping.Database;
import org.eclipse.jnosql.mapping.DatabaseType;
import java.util.function.Supplier;

@ApplicationScoped
@Alternative
@Priority(Interceptor.Priority.APPLICATION)
public class ManagerSupplier implements Supplier<DatabaseManager> {
    @Produces
    @Database(DatabaseType.DOCUMENT)
    @Default
    @Typed({DatabaseManager.class, MongoDBDocumentManager.class})
    public MongoDBDocumentManager get() {
        return DatabaseContainer.INSTANCE.get("hotel");
    }
}

Cucumber has the feature to allow injection using an ObjectFactory. Once we are using CDI, we will generate an implementation to create those classes using CDI. In this case, at the src/test/java/org/soujava/demos/mongodb/config, generate the WeldCucumberObjectFactory class.

package org.soujava.demos.mongodb.config;

import io.cucumber.core.backend.ObjectFactory;
import org.jboss.weld.environment.se.Weld;
import org.jboss.weld.environment.se.WeldContainer;

public class WeldCucumberObjectFactory implements ObjectFactory {
    private Weld weld;
    private WeldContainer container;
    @Override
    public void start() {
        weld = new Weld();
        container = weld.initialize();
    }

    @Override
    public void stop() {
        if (weld != null) {
            weld.shutdown();
        }
    }

    @Override
    public boolean addClass(Class<?> stepClass) {
        return true; // accept all step classes
    }

    @Override
    public <T> T getInstance(Class<T> type) {
        return (T) container.select(type).get();
    }
}

SPI loads this class, so we need to register our new class to be executed by Cucumber. Create the src/test/resources/META-INF/services and put the io.cucumber.core.backend.ObjectFactory file.

org.soujava.demos.mongodb.config.WeldCucumberObjectFactory

Also, at the src/test/resources/META-INF, generate the beans.xml file:

<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
       http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       bean-discovery-mode="annotated>
</beans>

The class on configuration is the class that will convert the table into the Room entities, where at the src/test/java/org/soujava/demos/mongodb, we will create the RoomDataTableMapper class:

package org.soujava.demos.mongodb;

import io.cucumber.java.DataTableType;
import jakarta.enterprise.context.ApplicationScoped;
import org.soujava.demos.mongodb.document.CleanStatus;
import org.soujava.demos.mongodb.document.Room;
import org.soujava.demos.mongodb.document.RoomStatus;
import org.soujava.demos.mongodb.document.RoomType;
import java.util.Map;

@ApplicationScoped
public class RoomDataTableMapper {
    @DataTableType
    public Room roomEntry(Map<String, String> entry) {
        return Room.builder()
                .number(Integer.parseInt(entry.get("number")))
                .type(RoomType.valueOf(entry.get("type")))
                .status(RoomStatus.valueOf(entry.get("status")))
                .cleanStatus(CleanStatus.valueOf(entry.get("cleanStatus")))
                .build();
    }
}

Step 3: Generate our first scenario test

The code infrastructure is ready, where we set the ObjectFactory using Weld, and the table mapper to convert the table into our entities. The next step is the test generation itself. As it’s necessary to highlight in the BDD methodology, we start with the test. Then we start the implementation, but once the focus is more on showing the tool with MongoDB than the methodology itself, we finalize this tutorial with what should be the first step. We will create our last class in this tutorial: the HotelRoomSteps.

package org.soujava.demos.mongodb;

import io.cucumber.java.Before;
import io.cucumber.java.en.*;
import jakarta.enterprise.context.ApplicationScoped;
import org.assertj.core.api.Assertions;
import org.soujava.demos.mongodb.document.*;
import jakarta.inject.Inject;
import java.util.List;
import java.util.Optional;

@ApplicationScoped
public class HotelRoomSteps {

    @Inject
    private RoomRepository repository;

    @Before
    public void cleanDatabase() {
        repository.deleteBy();
    }

    @Given("the hotel management system is operational")
    public void theHotelManagementSystemIsOperational() {
        Assertions.assertThat(repository).as("RoomRepository should be initialized").isNotNull();
    }

    @When("I register a room with number {int}")
    public void iRegisterARoomWithNumber(Integer number) {
        Room room = Room.builder()
                .number(number)
                .type(RoomType.STANDARD)
                .status(RoomStatus.AVAILABLE)
                .cleanStatus(CleanStatus.CLEAN)
                .build();
        repository.save(room);
    }

    @Then("the room with number {int} should appear in the room list")
    public void theRoomWithNumberShouldAppearInTheRoomList(Integer number) {
        List<Room> rooms = repository.findAll();
        Assertions.assertThat(rooms)
                .extracting(Room::getNumber)
                .contains(number);
    }

    @When("I register the following rooms:")
    public void iRegisterTheFollowingRooms(List<Room> rooms) {
        rooms.forEach(repository::save);
    }

    @Then("there should be {int} rooms available in the system")
    public void thereShouldBeRoomsAvailableInTheSystem(int expectedCount) {
        List<Room> rooms = repository.findAll();
        Assertions.assertThat(rooms).hasSize(expectedCount);
    }

    @Given("a room with number {int} is registered as {word}")
    public void aRoomWithNumberIsRegisteredAs(Integer number, String statusName) {
        RoomStatus status = RoomStatus.valueOf(statusName);
        Room room = Room.builder()
                .number(number)
                .type(RoomType.STANDARD)
                .status(status)
                .cleanStatus(CleanStatus.CLEAN)
                .build();
        repository.save(room);
    }

    @When("I mark the room {int} as {word}")
    public void iMarkTheRoomAs(Integer number, String newStatusName) {
        RoomStatus newStatus = RoomStatus.valueOf(newStatusName);
        Optional<Room> roomOpt = repository.findByNumber(number);
        Assertions.assertThat(roomOpt)
                .as("Room %s should exist", number)
                .isPresent();
        Room updatedRoom = roomOpt.orElseThrow();
        updatedRoom.update(newStatus);
        repository.save(updatedRoom);
    }

    @Then("the room {int} should be marked as {word}")
    public void theRoomShouldBeMarkedAs(Integer number, String expectedStatusName) {
        RoomStatus expectedStatus = RoomStatus.valueOf(expectedStatusName);
        Optional<Room> roomOpt = repository.findByNumber(number);
        Assertions.assertThat(roomOpt)
                .as("Room %s should exist", number)
                .isPresent()
                .get()
                .extracting(Room::getStatus)
                .isEqualTo(expectedStatus);
    }
}

Conclusion

Behavior-driven development (BDD) encourages us to look beyond code and concentrate on a shared understanding among stakeholders. By integrating Jakarta Data, Eclipse JNoSQL, and Cucumber, we have learned how to articulate business expectations through executable scenarios. These scenarios are written in plain language and linked to actual database operations. This approach not only guarantees technical accuracy but also fosters alignment among developers, testers, and domain experts. Furthermore, it links more with another methodology that I enjoyed that is about domain driven design, which I've written a new book about.

In this tutorial, you discovered how to model a hotel domain, store and query data in MongoDB, and connect behavior specifications with concrete database assertions—all without relying on complex frameworks. The outcome is a clean, testable, and business-oriented foundation for your application.

BDD reminds us that software development is not merely about meeting requirements; it is about clearly communicating intent. When business language is incorporated into our code and tests, we move closer to the ultimate goal of software engineering: creating systems that operate exactly as users expect.

Ready to explore the benefits of MongoDB Atlas? Get started now by trying MongoDB Atlas.

Access the source code used in this tutorial.

Any questions? Come chat with us in the MongoDB Community Forum.

References:

The post Introduction to Behavior Driving Development with Java and MongoDB appeared first on foojay.

]]>
https://foojay.io/today/introduction-to-behavior-driving-development-with-java-and-mongodb/feed/ 0
Introduction to Data-Driven Testing with Java and MongoDB https://foojay.io/today/introduction-to-data-driven-testing-with-java-and-mongodb/ https://foojay.io/today/introduction-to-data-driven-testing-with-java-and-mongodb/#respond Thu, 25 Sep 2025 13:53:57 +0000 https://foojay.io/?p=121318 Table of Contents PrerequisitesStep 1: Create the entitiesExplanation of annotations:Step 2: Create a database containerStep 3: Generate our first DDTConclusion As applications expand, the complexity of the rules they enforce also increases. In many systems, these rules are embedded within ...

The post Introduction to Data-Driven Testing with Java and MongoDB appeared first on foojay.

]]>
Table of Contents
PrerequisitesStep 1: Create the entitiesExplanation of annotations:Step 2: Create a database containerStep 3: Generate our first DDTConclusion

As applications expand, the complexity of the rules they enforce also increases. In many systems, these rules are embedded within the data, primarily in database queries that filter, join, or compute based on real-world conditions. However, the tests for these queries are often shallow, repetitive, or, worse yet, completely absent. When there is an error in the database logic, the application may still compile successfully, but the business can suffer significant consequences.

Data-driven testing (DDT) is important because it enables you to validate business behavior across various scenarios without having to duplicate test logic. When used in conjunction with a document database like MongoDB, it becomes a powerful tool to ensure that your queries align not only with the data structure but also with the intended business outcomes. For Java applications where persistence logic goes beyond basic CRUD operations, data-driven testing helps you safeguard what matters most: correctness, clarity, and confidence.

In this tutorial, you’ll:

  • Model a domain (Room, RoomType, RoomStatus).
  • Write semantic repository queries using Jakarta Data.
  • Run data-driven tests using JUnit 5 and AssertJ.
  • Validate MongoDB queries in isolation using Testcontainers and Weld.

You can find all the code presented in this tutorial in the GitHub repository:

git clone git@github.com:soujava/data-driven-test-mongodb.git

Prerequisites

For this tutorial, you’ll need:

  • Java 21.
  • Maven.
  • A MongoDB cluster.

You can use the following Docker command to start a standalone MongoDB instance:

docker run --rm -d --name mongodb-instance -p 27017:27017 mongo

Data-driven testing is a technique where a single test is executed with different input data sets to validate multiple scenarios. Instead of writing repetitive test methods, you define combinations of inputs and expected outcomes, making it easier to test edge cases, business rules, and regression scenarios with clarity and efficiency.

In this age, data is the heart of any software system. Ensuring consistency and validating the code based on several data conditions will increase your quality. In this process, we can use a database to inject the data as input, to check the queries themselves, and also store the results as a report.

This approach aligns naturally with MongoDB, where application logic frequently relies on query conditions—particularly when filtering based on domain-specific rules. In a typical Java application, these queries are embedded in repositories or services and are rarely tested beyond simple "find by ID" checks. DDT allows you to test the intent behind queries, such as a hotel management system: Is a room considered available? Does it match VIP criteria? Should it be cleaned?

In this tutorial, we’ll use a Java SE project—without any heavyweight frameworks—to demonstrate how to combine Jakarta Data, JNoSQL, and JUnit 5 to write expressive, testable queries against MongoDB. Our focus will be on clarity, maintainability, and aligning tests with the business language, not just with database fields.

Step 1: Create the entities

The first step is to create a plain Maven project, where we can use Maven Archetype quick start. After making the Maven project, the next step is to include Jupiter, Mockito, AssertJ, and Testcontainers for test proposals. For the Java integration and MongoDB, we will explore it using the Java Enterprise specification, Jakarta EE, where we will utilize both specifications, Jakarta NoSQL and Jakarta Data, both of which are implemented by Eclipse JNoSQL. We don't need to spend a considerable amount of time setting it up; you can clone the GitHub repository. The pom.xml shows the dependencies using Java with the Apache Maven Project:

<dependencies>
        <dependency>
            <groupId>net.datafaker</groupId>
            <artifactId>datafaker</artifactId>
            <version>2.4.4</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jnosql.databases</groupId>
            <artifactId>jnosql-mongodb</artifactId>
            <version>${jnosql.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.weld.se</groupId>
            <artifactId>weld-se-shaded</artifactId>
            <version>${weld.se.core.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse</groupId>
            <artifactId>yasson</artifactId>
            <version>3.0.4</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>io.smallrye.config</groupId>
            <artifactId>smallrye-config-core</artifactId>
            <version>3.13.3</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile.config</groupId>
            <artifactId>microprofile-config-api</artifactId>
            <version>3.1</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>${mockito.verson}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>${mockito.verson}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.weld</groupId>
            <artifactId>weld-junit5</artifactId>
            <version>5.0.1.Final</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>mongodb</artifactId>
            <version>1.21.3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.27.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

With the project done, the next step is setting and defining the entities. In our sample, we will explore a simple hotel management system, where we will extract some use cases to further explore the data-driven test. Naturally, a hotel management system brings way more complexity than that. Thus, we won't cover points such as payment. Therefore, we will create a Room entity and its enums that will bring the Value Object perspective.

In the src/main/java directory, create a Room class:

import jakarta.nosql.Column;
import jakarta.nosql.Entity;
import jakarta.nosql.Id;

import java.util.Objects;

@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;

}

public enum RoomStatus {
    AVAILABLE,
    RESERVED,
    UNDER_MAINTENANCE,
    OUT_OF_SERVICE
}

public enum RoomType {
    STANDARD,
    DELUXE,
    SUITE,
    VIP_SUITE
}

public enum CleanStatus {
    CLEAN,
    DIRTY,
    INSPECTION_NEEDED
}

Explanation of annotations:

  • @Entity: Marks the Room class as a database entity for management by Jakarta NoSQL.
  • @Id: Indicates the primary identifier for the entity, uniquely distinguishing each document in the MongoDB collection.
  • @Column: Maps fields (roomNumber, type) for reading from or writing to MongoDB.

Finally, with the entity done, we will create a repository where the goal is to find rooms by type, insert a new room, and check for availability on rooms—both standard and VIP rooms:

@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") RoomType type);
}

We have those queries that explore Jakarta Data Queries, but are they properly working? In the next step, we will generate some tests to check it.

Step 2: Create a database container

After the entity and repository are done, the next step is to generate the test structure. Use Testcontainer to ensure that the database is running when we use this container. We will generate an enum to implement the Singleton pattern, which will create a MongoDB database container and start a MongoDB instance for testing purposes. 

Thus, at src/test/java, create the DatabaseContainer class.

import org.eclipse.jnosql.communication.Settings;
import org.eclipse.jnosql.databases.mongodb.communication.MongoDBDocumentConfiguration;
import org.eclipse.jnosql.databases.mongodb.communication.MongoDBDocumentConfigurations;
import org.eclipse.jnosql.databases.mongodb.communication.MongoDBDocumentManager;
import org.eclipse.jnosql.databases.mongodb.communication.MongoDBDocumentManagerFactory;
import org.eclipse.jnosql.mapping.core.config.MappingConfigurations;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;

import java.util.HashMap;
import java.util.Map;
public enum DatabaseContainer {

    INSTANCE;

    private final GenericContainer<?> mongodb =
            new GenericContainer<>("mongo:latest")
                    .withExposedPorts(27017)
                    .waitingFor(Wait.defaultWaitStrategy());

    {
        mongodb.start();
    }

    public MongoDBDocumentManager get(String database) {
        Settings settings = getSettings(database);
        MongoDBDocumentConfiguration configuration = new MongoDBDocumentConfiguration();
        MongoDBDocumentManagerFactory factory = configuration.apply(settings);
        return factory.apply(database);
    }

    private Settings getSettings(String database) {
        Map<String,Object> settings = new HashMap<>();
        settings.put(MongoDBDocumentConfigurations.HOST.get()+".1", host());
        settings.put(MappingConfigurations.DOCUMENT_DATABASE.get(), database);
        return Settings.of(settings);
    }

    public String host() {
        return mongodb.getHost() + ":" + mongodb.getFirstMappedPort();
    }
}

In the code, we will generate a MongoDB database instance by container that we have a DatabaseConfiguration.

With the database container ready, the next step is to instruct CDI to use a MongoDB container from Testcontainer during testing, rather than any production configuration. We can do this by exploring the CDI capability of alternatives.

At src//test/java, create the ManagerSupplier class.

import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.Typed;
import jakarta.interceptor.Interceptor;
import org.eclipse.jnosql.communication.semistructured.DatabaseManager;
import org.eclipse.jnosql.databases.mongodb.communication.MongoDBDocumentManager;
import org.eclipse.jnosql.mapping.Database;
import org.eclipse.jnosql.mapping.DatabaseType;

import java.util.function.Supplier;

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

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

On this class, we can see that we are overwriting the behavior at test where we are using the DatabaseContainer where the database is called hotel. With the structure done, the next step is playing with tests and DDT.

Step 3: Generate our first DDT

One of the goals of data-driven testing is to achieve test coverage across various input combinations and capture the expected output. The first test we will generate is to verify that we are saving the information properly in the database. In this case, it does not matter which room is selected; it should insert and also generate an ID for it.

At src/test/java, create the RoomServiceTest class.

@EnableAutoWeld
@AddPackages(value = {Database.class, EntityConverter.class, DocumentTemplate.class, MongoDBTemplate.class})
@AddPackages(Room.class)
@AddPackages(ManagerSupplier.class)
@AddPackages(MongoDBTemplate.class)
@AddPackages(Reflections.class)
@AddPackages(Converters.class)
@AddExtensions({ReflectionEntityMetadataExtension.class, DocumentExtension.class})
class RoomServiceTest {

@Inject
private RoomRepository repository;

private static final Faker FAKER = new Faker();

@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());
    });
}

  static Stream<Arguments> room() {
        return Stream.of(Arguments.of(getRoom(), Arguments.of(getRoom(), Arguments.of(getRoom()))));
    }

    private static Room getRoom() {
        return new RoomBuilder()
                .roomNumber(FAKER.number().numberBetween(100, 999))
                .type(randomEnum(RoomType.class))
                .status(randomEnum(RoomStatus.class))
                .cleanStatus(randomEnum(CleanStatus.class))
                .smokingAllowed(FAKER.bool().bool())
                .build();
    }

private static <T extends Enum<?>> T randomEnum(Class<T> enumClass) {
    T[] constants = enumClass.getEnumConstants();
    int index = ThreadLocalRandom.current().nextInt(constants.length);
    return constants[index];
}

}

At the header of RoomService, we have a couple of annotations to activate Weld, the CDI implementation, in addition to which classes and packages the CDI should scan. It facilitates and makes the test startup lighter than scanning the whole class. Here, we are using AssertJ to further explore the fluent API for checking the database. We are using soft assertions that execute the whole validations and then show which conditions have break. It is way more useful when we need to do several validations in a single method.

We will generate a new test scenario that allows us to find rooms by type. Naturally, we want to ensure that it works for any kind of search. At the same class, we will generate a method where we will inject the enum by parameter, as you can see in the code below:

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

We are injecting several inputs to validate the tests, and we can explore it even further to see if the tests qualify: 

import jakarta.inject.Inject;
import net.datafaker.Faker;
import org.assertj.core.api.SoftAssertions;
import org.eclipse.jnosql.databases.mongodb.mapping.MongoDBTemplate;
import org.eclipse.jnosql.mapping.Database;
import org.eclipse.jnosql.mapping.core.Converters;
import org.eclipse.jnosql.mapping.document.DocumentTemplate;
import org.eclipse.jnosql.mapping.document.spi.DocumentExtension;
import org.eclipse.jnosql.mapping.reflection.Reflections;
import org.eclipse.jnosql.mapping.reflection.spi.ReflectionEntityMetadataExtension;
import org.eclipse.jnosql.mapping.semistructured.EntityConverter;
import org.jboss.weld.junit5.auto.AddExtensions;
import org.jboss.weld.junit5.auto.AddPackages;
import org.jboss.weld.junit5.auto.EnableAutoWeld;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Stream;

@EnableAutoWeld
@AddPackages(value = {Database.class, EntityConverter.class, DocumentTemplate.class, MongoDBTemplate.class})
@AddPackages(Room.class)
@AddPackages(ManagerSupplier.class)
@AddPackages(MongoDBTemplate.class)
@AddPackages(Reflections.class)
@AddPackages(Converters.class)
@AddExtensions({ReflectionEntityMetadataExtension.class, DocumentExtension.class})
class RoomServiceTest {

    @Inject
    private RoomRepository repository;

    private static final  Faker FAKER = new Faker();

    @BeforeEach
    void setUP() {
        Room vipRoom1 = new RoomBuilder()
                .roomNumber(101)
                .type(RoomType.VIP_SUITE)
                .status(RoomStatus.AVAILABLE)
                .cleanStatus(CleanStatus.CLEAN)
                .smokingAllowed(false)
                .build();

        Room vipRoom2 = new RoomBuilder()
                .roomNumber(102)
                .type(RoomType.VIP_SUITE)
                .status(RoomStatus.AVAILABLE)
                .cleanStatus(CleanStatus.CLEAN)
                .smokingAllowed(true)
                .build();

        Room standardRoom1 = new RoomBuilder()
                .roomNumber(201)
                .type(RoomType.STANDARD)
                .status(RoomStatus.AVAILABLE)
                .cleanStatus(CleanStatus.CLEAN)
                .smokingAllowed(false)
                .build();

        Room standardRoom2 = new RoomBuilder()
                .roomNumber(202)
                .type(RoomType.DELUXE)
                .status(RoomStatus.AVAILABLE)
                .cleanStatus(CleanStatus.CLEAN)
                .smokingAllowed(false)
                .build();

        Room dirtyReservedRoom = new RoomBuilder()
                .roomNumber(301)
                .type(RoomType.DELUXE)
                .status(RoomStatus.RESERVED)
                .cleanStatus(CleanStatus.DIRTY)
                .smokingAllowed(false)
                .build();

        Room dirtySuiteRoom = new RoomBuilder()
                .roomNumber(302)
                .type(RoomType.SUITE)
                .status(RoomStatus.UNDER_MAINTENANCE)
                .cleanStatus(CleanStatus.INSPECTION_NEEDED)
                .smokingAllowed(false)
                .build();

        Room smokingAllowedRoom = new RoomBuilder()
                .roomNumber(401)
                .type(RoomType.STANDARD)
                .status(RoomStatus.AVAILABLE)
                .cleanStatus(CleanStatus.CLEAN)
                .smokingAllowed(true)
                .build();

        repository.save(List.of(
                vipRoom1, vipRoom2,
                standardRoom1, standardRoom2,
                dirtyReservedRoom, dirtySuiteRoom,
                smokingAllowedRoom
        ));
    }

    @AfterEach
    void cleanUp() {
        repository.deleteBy();
    }

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

    @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());
        });
    }

    @Test
    void shouldFindRoomReadyToGuest() {
        List<Room> rooms = this.repository.findAvailableStandardRooms();
        SoftAssertions.assertSoftly(softly -> {
            softly.assertThat(rooms).hasSize(3);
            softly.assertThat(rooms).allMatch(room -> room.getStatus().equals(RoomStatus.AVAILABLE));
            softly.assertThat(rooms).allMatch(room -> !room.isUnderMaintenance());
        });
    }

    @Test
    void shouldFindVipRoomsReadyForGuests() {
        List<Room> rooms = this.repository.findVipRoomsReadyForGuests();
        SoftAssertions.assertSoftly(softly -> {
            softly.assertThat(rooms).hasSize(2);
            softly.assertThat(rooms).allMatch(room -> room.getType().equals(RoomType.VIP_SUITE));
            softly.assertThat(rooms).allMatch(room -> room.getStatus().equals(RoomStatus.AVAILABLE));
            softly.assertThat(rooms).allMatch(room -> !room.isUnderMaintenance());
        });
    }

    @Test
    void shouldFindAvailableSmokingRooms() {
        List<Room> rooms = this.repository.findAvailableSmokingRooms();
        SoftAssertions.assertSoftly(softly -> {
            softly.assertThat(rooms).hasSize(2);
            softly.assertThat(rooms).allMatch(room -> room.isSmokingAllowed());
            softly.assertThat(rooms).allMatch(room -> room.getStatus().equals(RoomStatus.AVAILABLE));
        });
    }

    @Test
    void shouldFindRoomsNeedingCleaning() {
        List<Room> rooms = this.repository.findRoomsNeedingCleaning();
        SoftAssertions.assertSoftly(softly -> {
            softly.assertThat(rooms).hasSize(2);
            softly.assertThat(rooms).allMatch(room -> !room.getCleanStatus().equals(CleanStatus.CLEAN));
            softly.assertThat(rooms).allMatch(room -> !room.getStatus().equals(RoomStatus.OUT_OF_SERVICE));
        });
    }

    static Stream<Arguments> room() {
        return Stream.of(Arguments.of(getRoom(), Arguments.of(getRoom(), Arguments.of(getRoom()))));
    }

    private static Room getRoom() {
        return new RoomBuilder()
                .roomNumber(FAKER.number().numberBetween(100, 999))
                .type(randomEnum(RoomType.class))
                .status(randomEnum(RoomStatus.class))
                .cleanStatus(randomEnum(CleanStatus.class))
                .smokingAllowed(FAKER.bool().bool())
                .build();
    }

    private static <T extends Enum<?>> T randomEnum(Class<T> enumClass) {
        T[] constants = enumClass.getEnumConstants();
        int index = ThreadLocalRandom.current().nextInt(constants.length);
        return constants[index];
    }
}

Conclusion

In software development, the gap between business rules and database logic is often where subtle bugs and misunderstandings live. By adopting data-driven testing, we shift the focus from checking technical details to validating actual business behavior—across a wide range of scenarios and edge cases.

In this tutorial, you learned how to apply this approach using Java SE, Jakarta Data, Eclipse JNoSQL, and MongoDB. You saw how to express queries with business semantics, isolate your tests using Testcontainers, and validate outcomes with JUnit 5 and AssertJ. More than just testing correctness, this style helps you design repositories and queries that align with the domain itself. That’s the real power of data-driven testing: It turns your tests into a source of clarity, documentation, and confidence.

Ready to explore the benefits of MongoDB Atlas? Get started now by trying MongoDB Atlas.

Access the source code used in this tutorial.

Any questions? Come chat with us in the MongoDB Community Forum.

References:

The post Introduction to Data-Driven Testing with Java and MongoDB appeared first on foojay.

]]>
https://foojay.io/today/introduction-to-data-driven-testing-with-java-and-mongodb/feed/ 0
Java Virtual Threads in Action: Optimizing MongoDB Operation https://foojay.io/today/java-virtual-threads-in-action-optimizing-mongodb-operation/ https://foojay.io/today/java-virtual-threads-in-action-optimizing-mongodb-operation/#respond Tue, 01 Jul 2025 08:12:24 +0000 https://foojay.io/?p=116729 Table of Contents PrerequisitesStep 1: Create the Product entityStep 2: Create the ServiceStep 3: Expose the Camera APIStep 4: Build and run the applicationStep 5: Test the APIConclusion Virtual threads have become one of the most popular resources in Java ...

The post Java Virtual Threads in Action: Optimizing MongoDB Operation appeared first on foojay.

]]>
Table of Contents
PrerequisitesStep 1: Create the Product entityStep 2: Create the ServiceStep 3: Expose the Camera APIStep 4: Build and run the applicationStep 5: Test the APIConclusion

Virtual threads have become one of the most popular resources in Java and are trending inside the language. Indeed, this resource introduced a cheap way to create threads inside the JVM. In this tutorial, we will explain how to use it with MongoDB.

You can find all the code presented in this tutorial in the GitHub repository:

git clone git@github.com:soujava/mongodb-virtual-threads.git

Prerequisites

For this tutorial, you’ll need:

You can use the following Docker command to start a standalone MongoDB instance:

docker run --rm -d --name mongodb-instance -p 27017:27017 mongo

Java 21 has introduced a new era of concurrency with virtual threads—lightweight threads managed by the JVM that significantly enhance the performance of I/O-bound applications. Unlike traditional platform threads, which are directly linked to operating system threads, virtual threads are designed to be inexpensive and can number in the thousands. This enables you to manage many concurrent operations without the typical overhead of traditional threading.

Virtual threads are scheduled on a small pool of carrier threads, ensuring that blocking operations—such as those commonly encountered when interacting with databases—do not waste valuable system resources.

In this tutorial, we will generate a Quarkus project that leverages Java 21’s virtual threads to build a highly responsive, non-blocking application integrated with MongoDB via Eclipse JNoSQL. The focus is on exploring the benefits of virtual threads in managing I/O-bound workloads and illustrating how modern Java concurrency can transform database interactions by reducing latency and improving scalability.
As a first step, follow the guide, MongoDB Developer: Quarkus & Eclipse JNoSQL. This will help you set up the foundation of your Quarkus project. After that, you'll integrate Java 21 features to fully exploit the power of virtual threads in your MongoDB-based application.

During the creation process, ensure you generate the project's latest version on both Quarkus and Eclipse JNoSQL. Make sure that you have a version higher than:

<dependency>
   <groupId>io.quarkiverse.jnosql</groupId>
   <artifactId>quarkus-jnosql-document-mongodb</artifactId>
   <version>3.3.4</version>
</dependency>

In this tutorial, we will generate services to handle cameras. We will create cameras based on Datafaker and return all the cameras using virtual threads with Quarkus. In your project, locate the application.properties file (usually under src/main/resources) and add the following line:

# Define the database name
jnosql.document.database=cameras

With this foundation, we'll move on to implementing the product entity and explore how MongoDB's embedded types can simplify data modeling for your application.

Step 1: Create the Product entity

To start, we’ll define the core of our application: the Camera entity. This class represents the camera data structure and contains fields such as id, brand, model, and brandWithModel. We will have a static factory method where, based on the Faker instance, it will generate a Camera instance.

In the src/main/java directory, create a Camera class:

import jakarta.nosql.Column;
import jakarta.nosql.Entity;
import jakarta.nosql.Id;
import net.datafaker.Faker;

import java.time.LocalDate;
import java.util.Objects;
import java.util.UUID;

@Entity
public record Camera(
        @Id @Convert(ObjectIdConverter.class) String id,
        @Column String brand,
        @Column String model,
        @Column String brandWithModel
) {

    public static Camera of(Faker faker) {
        var camera = faker.camera();
        String brand = camera.brand();
        String model = camera.model();
        String brandWithModel = camera.brandWithModel();
        return new Camera(UUID.randomUUID().toString(), brand, model, brandWithModel);
    }

    public Camera update(Camera request) {
        return new Camera(this.id, request.brand, request.model, request.brandWithModel);
    }
}

Explanation of annotations:

  • @Entity: Marks the Product class as a database entity for management by Jakarta NoSQL.
  • @Column: Maps fields (name, manufacturer, tags, categories) for reading from or writing to MongoDB.

Step 2: Create the Service

In our application, the CameraService class serves as a bridge between our business logic and MongoDB. We utilize Eclipse JNoSQL, which supports Jakarta NoSQL and Jakarta Data. In this tutorial, we work with the DocumentTemplate interface from Jakarta NoSQL—a specialized version of the generic Template interface tailored for NoSQL document capabilities. The Quarkus integration makes it easier once you, as a Java developer, need to use an injection annotation.

Furthermore, we inject an ExecutorService that is qualified with the @VirtualThreads annotation. This annotation instructs Quarkus to provide an executor that employs Java 21's virtual threads for improved concurrency.

Define a CameraService to interact with MongoDB:

import io.quarkus.virtual.threads.VirtualThreads;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import net.datafaker.Faker;
import org.eclipse.jnosql.mapping.document.DocumentTemplate;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.logging.Logger;

@ApplicationScoped
public class CameraService {

    private static final Logger LOGGER = Logger.getLogger(CameraService.class.getName());

    private static final Faker FAKER = new Faker();

    @Inject
    DocumentTemplate template;

    @Inject
    @VirtualThreads
    ExecutorService vThreads;

    public List<Camera> findAll() {
        LOGGER.info("Selecting all cameras");
        return template.select(Camera.class).result();
    }

    public List<Camera> findByBrand(String brand) {
        LOGGER.info("Selecting cameras by brand: " + brand);
        return template.select(Camera.class)
                .where("brand")
                .like(brand)
                .result();
    }

    public Optional<Camera> findById(String id) {
        var camera =  template.find(Camera.class, id);
        LOGGER.info("Selecting camera by id: " + id + " find? " + camera.isPresent());
        return camera;
    }

    public void deleteById(String id) {
        LOGGER.info("Deleting camera by id: " + id);
        template.delete(Camera.class, id);
    }

    public Camera insert(Camera camera) {
        LOGGER.info("Inserting camera: " + camera.id());
        return template.insert(camera);
    }

    public Camera update(Camera update) {
        LOGGER.info("Updating camera: " + update.id());
        return template.update(update);
    }

    public void insertAsync(int size) {
        LOGGER.info("Inserting cameras async the size: " + size);

        for (int index = 0; index < size; index++) {
            vThreads.submit(() -> {
                Camera camera = Camera.of(FAKER);
                template.insert(camera);
            });
        }
    }
}

In this code:

  • The DocumentTemplate provides the necessary operations (CRUD) to interact with MongoDB.
  • The vThreads ExecutorService, annotated with @VirtualThreads, submits tasks that insert fake camera records asynchronously. This is a prime example of how virtual threads can be leveraged for I/O-bound operations without manual thread management.

This setup shows how Quarkus and Eclipse JNoSQL simplify the development process: You get the benefits of Jakarta NoSQL for MongoDB without the boilerplate, and virtual threads allow you to write scalable, concurrent applications in a natural, synchronous style.

For more details on using virtual threads in Quarkus, check out the Quarkus Virtual Threads Guide.

Step 3: Expose the Camera API

We’ll create the CameraResource class to expose our data through a RESTful API. This resource handles HTTP requests. We will generate a camera either manually or using the asynchronous resource. You can define the size of the cameras generated, with the default being 100.

Create a CameraResource class to handle HTTP requests:

import io.quarkus.virtual.threads.VirtualThreads;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import java.util.List;

@Path("cameras")
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public class CameraResource {

    @Inject
    CameraService service;

    @GET
    @VirtualThreads
    public List<Camera> findAll() {
        return service.findAll();
    }

    @GET
    @Path("brand/{brand}")
    public List<Camera> listAll(@PathParam("brand") String brand) {
        if (brand == null || brand.isBlank()) {
            return service.findAll();
        }
        return service.findByBrand(brand);
    }


    @POST
    public Camera add(Camera camera) {
        return service.insert(camera);
    }

    @Path("{id}")
    @GET
    public Camera get(@PathParam("id") String id) {
        return service.findById(id)
                .orElseThrow(() -> new WebApplicationException(Response.Status.NOT_FOUND));
    }

    @Path("{id}")
    @PUT
    public Camera update(@PathParam("id") String id, Camera request) {
        var camera = service.findById(id)
                .orElseThrow(() -> new WebApplicationException(Response.Status.NOT_FOUND));
        return service.update(camera.update(request));

    }

    @Path("{id}")
    @DELETE
    public void delete(@PathParam("id") String id) {
        service.deleteById(id);
    }

    @POST
    @Path("async")
    public Response insertCamerasAsync(@QueryParam("size") @DefaultValue("100") int size) {
        service.insertAsync(size);
        return Response.accepted("Insertion of " + size + " cameras initiated.").build();
    }

}

Step 4: Build and run the application

It’s finally time to integrate everything and run the application. After packaging the project with Maven, start the application and ensure that MongoDB runs locally or through MongoDB Atlas. Once the application runs, you can test the API endpoints to interact with the camera data.

Make sure MongoDB is running (locally or on MongoDB Atlas). Then, build and run the application:

mvn clean package -Dquarkus.package.type=uber-jar
java -jar target/mongodb-virtual-thread-1.0.1-runner.jar

Step 5: Test the API

Create a Camera

curl -X POST -H "Content-Type: application/json" -d '{
  "brand": "Canon",
  "model": "EOS 5D Mark IV",
  "brandWithModel": "Canon EOS 5D Mark IV"
}' http://localhost:8080/cameras

Get all Cameras

curl -X GET http://localhost:8080/cameras

Get Cameras by Brand

curl -X GET http://localhost:8080/cameras/brand/Canon

Get a Camera by ID

curl -X GET http://localhost:8080/cameras/{id}

Update a Camera by ID

curl -X PUT -H "Content-Type: application/json" -d '{
  "brand": "Nikon",
  "model": "D850",
  "brandWithModel": "Nikon D850"
}' http://localhost:8080/cameras/{id}

Delete a Camera by ID

curl -X DELETE http://localhost:8080/cameras/{id}

Insert Cameras asynchronously
This endpoint triggers the asynchronous insertion of fake camera records. The size parameter defaults to 100 if you omit it.

curl -X POST http://localhost:8080/cameras/async?size=100

Conclusion

Java 21's virtual threads simplify handling I/O-bound operations, allowing for massive concurrency with minimal overhead. By integrating virtual threads with MongoDB and Quarkus while maintaining a clean, synchronous programming model, we built a scalable and responsive application.

Ready to explore the benefits of MongoDB Atlas? Get started now by trying MongoDB Atlas.

Access the source code used in this tutorial.

Any questions? Come chat with us in the MongoDB Community Forum.

The post Java Virtual Threads in Action: Optimizing MongoDB Operation appeared first on foojay.

]]>
https://foojay.io/today/java-virtual-threads-in-action-optimizing-mongodb-operation/feed/ 0