The current state of testing with Couchbase requires you to use something like CouchbaseMock, or mock the API yourself, or have a running Couchbase Server instance started prior to running those tests. Mocking works but is not really testing Couchbase. It might be ok for unit test but ruled out for integration tests. Having a Couchbase instance started is better. And while this works, it is not an easy, industrializable solution. You can’t just run a build on any machine and expect the thing to work. It would require everyone to install Couchbase Server and have it online all the time. And while this would make me very happy, it’s unlikely to happen.

So another approach could be to have your test responsible for starting the database. You could certainly include that in your build scripts. Maven, Gradle and others can provide you hooks on the build lifecycle. This way you could start and stop the DB before running integration test. But it would add some dependency to the buid tool you are using. To be build tool independant, you need to start and stop the DB from your code.

Another problem you will encounter while setting this up is that you likely have to support different OSs. Starting and stoping the DB will differ if you are running Linux, Windows or OSX. And this is assuming that the DB is already installed on the machine. To avoid those issues, you need a common runtime to all these platforms. And this is something Docker can give you.

Docker will act as a distributed binary store to download an image for any DB you want. And the way you manage Docker container is identical across platform. It provides a super lightweight alternative to VMs and while it might give you trouble in some particular cases in production, it’s absolutely perfect for this use case.

The great people behing TestContainers have understand that and are proposing an integrated solution for all the problems above.

TestContainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.

TestContainers is currently only Java but this concept can be adapted to any language having a Docker client. Let’s see how this works with a simple Java project.

How to Test the Beer-sample

I wrote a project showing everything you need to use TestContainers and Couchbase. For this project to work you will need Docker to run on the machine executing the test. To make sure it works, open a terminal and type docker info . This should give you an answer like this:

Now let’s write the test. We can start by defining a GenericContainer instance with a @ClassRule. It means the container will be setup before running the tests and teared down after the tests. You can use @Rule if you want this to happen for each test of the class you are running. This GenericContainer requires a name. This is the full identifier, name and tag, of the Docker image you want to use. Here I am using a custom image that starts Couchbase Server with the beer-sample preloaded. You can build that image by typing docker build -t mycouchbase . at the root of the project.

Next you can setup the list of ports you want to expose and a waiting strategy. This waiting strategy part is quite important and we will come back to it later.

public class ExampleTest { @ClassRule public static GenericContainer couchbase = new GenericContainer("mycouchbase:latest") .withExposedPorts(8091, 8092, 8093, 8094, 11207, 11210, 11211, 18091, 18092, 18093) .waitingFor(new CouchbaseWaitStrategy()); @Test public void beerBucketTest() throws InterruptedException { CouchbaseEnvironment env = DefaultCouchbaseEnvironment.builder() .bootstrapCarrierDirectPort(couchbase.getMappedPort(11210)) .bootstrapCarrierSslPort(couchbase.getMappedPort(11207)) .bootstrapHttpDirectPort(couchbase.getMappedPort(8091)) .bootstrapHttpSslPort(couchbase.getMappedPort(18091)) .queryPort(couchbase.getMappedPort(8093)) .build(); CouchbaseCluster cc = CouchbaseCluster.create(env); ClusterManager cm = cc.clusterManager("Administrator", "password"); assertTrue(cm.hasBucket("beer-sample")); Bucket bucket = cc.openBucket("beer-sample"); assertTrue(bucket.exists("21st_amendment_brewery_cafe")); bucket.close(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class ExampleTest { @ ClassRule public static GenericContainer couchbase = new GenericContainer ( "mycouchbase:latest" ) . withExposedPorts ( 8091 , 8092 , 8093 , 8094 , 11207 , 11210 , 11211 , 18091 , 18092 , 18093 ) . waitingFor ( new CouchbaseWaitStrategy ( ) ) ; @ Test public void beerBucketTest ( ) throws InterruptedException { CouchbaseEnvironment env = DefaultCouchbaseEnvironment . builder ( ) . bootstrapCarrierDirectPort ( couchbase . getMappedPort ( 11210 ) ) . bootstrapCarrierSslPort ( couchbase . getMappedPort ( 11207 ) ) . bootstrapHttpDirectPort ( couchbase . getMappedPort ( 8091 ) ) . bootstrapHttpSslPort ( couchbase . getMappedPort ( 18091 ) ) . queryPort ( couchbase . getMappedPort ( 8093 ) ) . build ( ) ; CouchbaseCluster cc = CouchbaseCluster . create ( env ) ; ClusterManager cm = cc . clusterManager ( "Administrator" , "password" ) ; assertTrue ( cm . hasBucket ( "beer-sample" ) ) ; Bucket bucket = cc . openBucket ( "beer-sample" ) ; assertTrue ( bucket . exists ( "21st_amendment_brewery_cafe" ) ) ; bucket . close ( ) ; }

Then you have the test method. I start by defining a new CouchbaseEnvironment. Since all the exposed ports have been mapped to a different one, we need to specify them. You can get the mapped port by simply calling getMappedPort(yourPort) on the GenericContainer. Than I simply create my CouchbaseCluster and go on with my test.

This only works because I added a custom wait strategy by calling waitingFor(new CouchbaseWaitStrategy()) . By default TestContainers allow you to wait for a port to be accessible or for a call to an URL to return a particular status code. This is unfortunately not sufficient for Couchbase. When you start a Couchbase server there is a warmup phase for the nodes of your cluster. A GET on http://couchabseserver:8091/ui/index.html would return a 200 status code or the port 8091 would answer while your node would still be in warmup phase, thus inaccessible from the SDK.

It means we need a specific wait strategy to know if the node’s status is healthy. To know it a node is healthy you can GET the following URL http://couchabseserver:8091/pools/default/ . It returns a JSON with informations about the nodes of your cluster. It means we need a wait strategy that get that JSON and test if the node’s status is ‘healthy’. It like using a regular HTTPWaitStrategy with some additional behavior.

Unfortunately this class is mostly made of private and protected fields, making it hard to extend in my project. I unfortunately had to duplicate it and add my own logic. You can find the full code of the wait strategy on Github. This is the bit I have added.

// Specific Couchbase wait strategy to be sure the node is online and healthy JsonNode node = om.readTree(connection.getInputStream()); JsonNode statusNode = node.at("/nodes/0/status"); String status = statusNode.asText(); if (!"healthy".equals(status)){ throw new RuntimeException(String.format("Couchbase Node status was: %s", status)); } 1 2 3 4 5 6 7 8 // Specific Couchbase wait strategy to be sure the node is online and healthy JsonNode node = om . readTree ( connection . getInputStream ( ) ) ; JsonNode statusNode = node . at ( "/nodes/0/status" ) ; String status = statusNode . asText ( ) ; if ( ! "healthy" . equals ( status ) ) { throw new RuntimeException ( String . format ( "Couchbase Node status was: %s" , status ) ) ; }

Conclusion

This code is still pretty specific and requires you to build your own Couchbase image, already installed and containing data. Having premade images with data ready for integration testing can be super useful, especially for integration tests. You might want to have a lighter approach for unit tests though. TestContainers have already several modules specific to particular DBs. We’ll try to make one for Couchbase that does not require a particular image and allow you to setup our default image the way you want for your tests.

Please let us know if you would like such features!