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

Onur Baskirt is a Software Engineering Leader with international experience in world-class companies. Now, he is a Software Engineering Lead at Emirates Airlines in Dubai.