Couchbase Testcontainers in Spring Boot Tests with JUnit 5

Hi all, in this article I will explain how to integrate spring boot tests with Testcontainers. In this example, I will use the Couchbase module of Testcontainers and create a running instance of Couchbase locally before the test executions. In this way, the tests will communicate with the locally and dynamically running disposable Couchbase container rather than connecting a real database. Let’s learn and implement the Couchbase Testcontainers in Spring Boot Tests with JUnit 5!

Why Should We Mock the Database in Tests?

In microservices testing strategies, we should write component tests, and in component tests, we need to use Test Doubles. (The Test Double is the generic term for any pretend object used in place of a real object for testing purposes.)

In these tests, we should isolate our service from the outside world and test the service functionalities in isolation. We can use several tools for mocking other services or downstream systems like WireMock, but we cannot use these tools for databases. We need to use specific solutions like CouchbaseMock, Mockito, etc., but mocking complex scenarios with these tools is burdensome. That’s why we should seek better solutions that provide easy mocking for DBs.

Testcontainers DB modules are one of the best fit for this purpose, and we can use a disposable database container that can be started and stopped in the runtime in our tests. Testcontainers has many modules, and in this article, I will show an example with the Couchbase module.

The Technology Stack for Coucbase Testcontainers in Spring Boot Tests

Service: Java Spring Boot Web Flux Service – Spring boot version is 2.6.3 (Spring version > 2.2.6)

Test Runner Library: Junit 5 Jupiter

Spring Boot REST API Test Client Library: WebTestClient

Mocking Library: Spring Cloud Contract WireMock

Testcontainers Modules: Testcontainers Junit Jupiter, Testcontainers Couchbase

Asynchronous Waiting Library: Awaitility

Required Libraries in Pom.xml

You need to add the below dependencies in your project. When I am writing this article, the below versions are the latest ones and you can check the latest versions on the maven repository webpage.

<!-- Test Containers dependencies -->
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.16.3</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.16.3</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>couchbase</artifactId>
    <version>1.16.3</version>
    <scope>test</scope>
</dependency>

<!-- Awaility dependency -->
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>4.1.1</version>
    <scope>test</scope>
</dependency>

Along with these libraries, in my case, I have Spring Boot Webflux, Junit Jupiter, Spring Cloud Wiremock libraries in my pom.xml to test Spring Boot Reactive WebFlux REST APIs.

Testcontainers Properties Setup

We either use application.properties or application.yml configuration files in the spring boot projects. In my case, our application configuration file is YAML-based, and the below properties are the Couchbase default properties.

Configuration properties below are specific to our project structure. In your case, these may be different. You may use the .properties config file and different Couchbase properties. These will not change the testcontainers integration approach, which we will see in the following chapters.

We will override these configuration properties in the runtime dynamically with Spring framework’s (version should be bigger than 2.2.6) @DynamicPropertySource annotation.

couchbase:
  cluster1: XX.XX.XX (You can write here your Couchbase Ips)
  bootstrapHttpDirectPort: 8091
  bootstrapHttpSslPort: 18091
  bootstrapCarrierDirectPort: 11210
  bootstrapCarrierSslPort: 11207
  bucket:
    usersession:
      name: Your couchbase username
      password: Your couchbase password
    configuration:
      name: Your couchbase username
      password: Your couchbase password
  bucketOpenTimeout: 25000
  operationTimeout: 60000
  observableTimeoutMilliSeconds: 65000
  ioPoolSize: 3
  computationPoolSize: 3

Testcontainers BaseTest Setup

The below base test class is the main class that handles all operations needed to start the Couchbase test container in the runtime. I will explain the code snippets of the class and then share the class’s whole code.

First, I defined the bucket name, username, password, and Couchbase image name. You can find Couchbase Docker images here.

static private final String           couchbaseBucketName        = "mybucket";
static private final String           username                   = "onur";
static private final String           password                   = "password1234";
private static final BucketDefinition bucketDefinition           = new BucketDefinition(couchbaseBucketName);
private static final DockerImageName  COUCHBASE_IMAGE_ENTERPRISE = DockerImageName.parse("couchbase:enterprise")
    .asCompatibleSubstituteFor("couchbase/server")
    .withTag("6.0.1");

Then, I declared the Couchbase container. 

//Define the couchbase container.
final static CouchbaseContainer couchbaseContainer = new CouchbaseContainer(COUCHBASE_IMAGE_ENTERPRISE)
    .withCredentials(username, password)
    .withBucket(bucketDefinition)
    .withStartupTimeout(Duration.ofSeconds(90))
    .waitingFor(Wait.forHealthcheck());

After the tests, I stopped the container by using Junit Jupiter’s @AfterAll annotation.

@AfterAll
public static void teardown() {
    couchbaseContainer.stop();
}

And the most critical part is to override the Couchbase configurations in the runtime via @DynamicPropertySource annotation.

@DynamicPropertySource
static void bindCouchbaseProperties(DynamicPropertyRegistry registry) {
    //Start the Couchbase container and wait until it is running.
    couchbaseContainer.start();
    await().until(couchbaseContainer::isRunning);

    //Get the randomly created container ports to override default port numbers.
    int bootstrapHttpSslPort = couchbaseContainer.getMappedPort(18091);
    int bootstrapCarrierSslPort = couchbaseContainer.getMappedPort(11207);

    //Couchbase properties overriding based on couchbase container.
    registry.add("couchbase.cluster1", couchbaseContainer::getContainerIpAddress);
    registry.add("couchbase.bootstrapHttpDirectPort", couchbaseContainer::getBootstrapHttpDirectPort);
    registry.add("couchbase.bootstrapHttpSslPort", () -> bootstrapHttpSslPort);
    registry.add("couchbase.bootstrapCarrierDirectPort", couchbaseContainer::getBootstrapCarrierDirectPort);
    registry.add("couchbase.bootstrapCarrierSslPort", () -> bootstrapCarrierSslPort);
    registry.add("couchbase.bucket.usersession.name", couchbaseContainer::getUsername);
    registry.add("couchbase.bucket.usersession.password", couchbaseContainer::getPassword);
    registry.add("couchbase.bucket.configuration.name", couchbaseContainer::getUsername);
    registry.add("couchbase.bucket.configuration.password", couchbaseContainer::getPassword);
    registry.add("couchbase.bucket.bucketOpenTimeout", () -> 250000);
    registry.add("couchbase.bucket.operationTimeout", () -> 600000);
    registry.add("couchbase.bucket.observableTimeoutMilliSeconds", () -> 650000);
    registry.add("couchbase.bucket.ioPoolSize", () -> 3);
    registry.add("couchbase.bucket.computationPoolSize", () -> 3);

In the first two lines of this method, I started the couchbase container and waited until it ran by using the awaitility library. @DynamicPropertySource works before the JUnit Jupiter’s @BoforeAll annotation; that’s why I could not start the Couchbase container via Junit Jupiter’s @BeforeAll annotation, and I moved the container start code to the beginning of the bindCouchbaseProperties method. Then, I used the awaitility library’s “await().until method” to wait until the container started, and finally, after this step, I overrode the default couchbase properties.

Rather than the above approach, if you don’t have @AutoConfigureWiremock annotation for your tests, you can annotate this class with the @TestContainers annotation and declare a container with the @Container annotation as shown below.

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = { Application.class })
@ComponentTest
class TestContainersBase {
    static private final String           couchbaseBucketName        = "mybucket";
    static private final String           username                   = "onur";
    static private final String           password                   = "password1234";
    private static final BucketDefinition bucketDefinition           = new BucketDefinition(couchbaseBucketName);
    private static final DockerImageName  COUCHBASE_IMAGE_ENTERPRISE = DockerImageName.parse("couchbase:enterprise")
        .asCompatibleSubstituteFor("couchbase/server")
        .withTag("6.0.1");

    @Container
    final static CouchbaseContainer couchbaseContainer = new CouchbaseContainer(COUCHBASE_IMAGE_ENTERPRISE)
        .withCredentials(username, password)
        .withBucket(bucketDefinition)
        .withStartupTimeout(Duration.ofSeconds(90))
        .waitingFor(Wait.forHealthcheck());

In my case, I used the @AutoConfigureWiremock annotation to automatically configure the Wiremock server to mock other services and external dependencies for component tests. Typically, the wiremock server and couchbase container should run in the order below.

@BeforeAll
public static void setup(){
    couchbaseContainer.start();
    wireMockServer = new WireMockServer(0);
    wireMockServer.start();
}

@AfterAll
public static void teardown(){
    couchbaseContainer.stop();
    wireMockServer.stop();
}

But, when I used @AutoConfigureWiremock and @TestContainers annotation annotations together, this order was not maintained, and that’s why I decided not to use @TestContainers and @Container annotations and manually start and stop the couchbase container in TestContainersBase class.

I also have @ComponentTest custom annotation in my tests, and this is entirely related to our technology stack, not to testcontainers.

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("ComponentTest")
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, classes = Application.class)
@ActiveProfiles(value = "component", resolver = SystemPropertyActiveProfileResolver.class)
@AutoConfigureWebTestClient(timeout = "30000")
@AutoConfigureWireMock(port = 0)
@Import(ElapsedTimeAspect.class)
public @interface ComponentTest {
}

In this annotation, I manage the SpringBootTest, WebTestClient auto-configuration, Wiremock autoconfiguration, ActiveProfile resolving (default profile is application-component.yaml), etc.

So, in the end, the TestContainersBase class will look like this.

@ComponentTest
abstract class TestContainersBase {
    static private final String           couchbaseBucketName        = "mybucket";
    static private final String           username                   = "onur";
    static private final String           password                   = "password1234";
    private static final BucketDefinition bucketDefinition           = new BucketDefinition(couchbaseBucketName);
    private static final DockerImageName  COUCHBASE_IMAGE_ENTERPRISE = DockerImageName.parse("couchbase:enterprise")
        .asCompatibleSubstituteFor("couchbase/server")
        .withTag("6.0.1");

    //Define the couchbase container.
    final static CouchbaseContainer couchbaseContainer = new CouchbaseContainer(COUCHBASE_IMAGE_ENTERPRISE)
        .withCredentials(username, password)
        .withBucket(bucketDefinition)
        .withStartupTimeout(Duration.ofSeconds(90))
        .waitingFor(Wait.forHealthcheck());

    @AfterAll
    public static void teardown() {
        couchbaseContainer.stop();
    }

    @DynamicPropertySource
    static void bindCouchbaseProperties(DynamicPropertyRegistry registry) {
        //Start the Couchbase container and wait until it is running.
        couchbaseContainer.start();
        await().until(couchbaseContainer::isRunning);

        //Get the randomly created container ports to override default port numbers.
        int bootstrapHttpSslPort = couchbaseContainer.getMappedPort(18091);
        int bootstrapCarrierSslPort = couchbaseContainer.getMappedPort(11207);

        //Couchbase properties overriding based on couchbase container.
        registry.add("couchbase.cluster1", couchbaseContainer::getContainerIpAddress);
        registry.add("couchbase.bootstrapHttpDirectPort", couchbaseContainer::getBootstrapHttpDirectPort);
        registry.add("couchbase.bootstrapHttpSslPort", () -> bootstrapHttpSslPort);
        registry.add("couchbase.bootstrapCarrierDirectPort", couchbaseContainer::getBootstrapCarrierDirectPort);
        registry.add("couchbase.bootstrapCarrierSslPort", () -> bootstrapCarrierSslPort);
        registry.add("couchbase.bucket.usersession.name", couchbaseContainer::getUsername);
        registry.add("couchbase.bucket.usersession.password", couchbaseContainer::getPassword);
        registry.add("couchbase.bucket.configuration.name", couchbaseContainer::getUsername);
        registry.add("couchbase.bucket.configuration.password", couchbaseContainer::getPassword);
        registry.add("couchbase.bucket.bucketOpenTimeout", () -> 250000);
        registry.add("couchbase.bucket.operationTimeout", () -> 600000);
        registry.add("couchbase.bucket.observableTimeoutMilliSeconds", () -> 650000);
        registry.add("couchbase.bucket.ioPoolSize", () -> 3);
        registry.add("couchbase.bucket.computationPoolSize", () -> 3);
    }
}

After this step, I extended this class and ran my tests. The tests started to connect the local couchbase container rather than the real couchbase cluster. One important point: before starting the tests, you need to install Docker and start it.

You can test this setup with an empty test like below.

@Test
void contextLoads(){
    
}

When you run the test, you will see the logs on your console, as shown in the screenshot below.

Couchbase Testcontainers in Spring Boot Junit 5 tests

Couchbase Connection and Cluster Setup in Main Code

Couchbase usage in your projects may vary based on your company’s development approach. In our case, we have Couchbase Environment Configuration class to read couchbase properties, and then we use these properties to create a couchbase cluster.

Environment Configuration Class

@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "couchbase")
@PropertySource(ResourceUtils.CLASSPATH_URL_PREFIX + "couchbase.properties")
public class EnvironmentConfiguration {

    private String cluster1;
    private int    bootstrapHttpDirectPort;
    private int    bootstrapHttpSslPort;
    private int    bootstrapCarrierDirectPort;
    private int    bootstrapCarrierSslPort;

    //I removed other config properties. Those are not related with couchbase testcontainers.
}

Couchbase Cluster Creation and Properties Overriding Parts

Each time the Couchbase Testcontainer container starts with random ports; that’s why we need to override the default couchbase ports with these randomly created ports to connect the Coucbase docker container. You can see this in the code snippet below.

@PostConstruct
private void init() {
    Objects.requireNonNull(envConfig.getCluster1(), "Missed mandatory couchbase:cluster1 property in application configuration");
    cluster = CouchbaseCluster.create(getCouchbaseEnvironment(), envConfig.getCluster1());
}
/**
 * Gets couchbase environment.
 *
 * @return the couchbase environment
 */
protected CouchbaseEnvironment getCouchbaseEnvironment() {
    final DefaultCouchbaseEnvironment.Builder cbEnvironmentBuilder = DefaultCouchbaseEnvironment
        .builder();
    final Optional<EnvironmentConfiguration> optionalEnvConfig = Optional.of(envConfig);
    optionalEnvConfig
        .map(EnvironmentConfiguration::getBootstrapHttpDirectPort)
        .ifPresent(cbEnvironmentBuilder::bootstrapHttpDirectPort);
    optionalEnvConfig
        .map(EnvironmentConfiguration::getBootstrapHttpSslPort)
        .ifPresent(cbEnvironmentBuilder::bootstrapHttpSslPort);
    optionalEnvConfig
        .map(EnvironmentConfiguration::getBootstrapCarrierDirectPort)
        .ifPresent(cbEnvironmentBuilder::bootstrapCarrierDirectPort);
    optionalEnvConfig
        .map(EnvironmentConfiguration::getBootstrapCarrierSslPort)
        .ifPresent(cbEnvironmentBuilder::bootstrapCarrierSslPort);
    return cbEnvironmentBuilder.build();
}

That’s all for this article. 

Happy testing!
Onur Baskirt

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.