Adopt modern DevOps practices
Oftentimes writing tests seem like just a nuisance you do after finishing a feature, before heading off to Confluence and JIRA to document your work. Sometimes (rarely, but still) you are right.
In a team of few co-located developers working on a tidy small to medium-sized project, verbosely organized (multiple testing environments, in-house QA team, etc.) you can easily get away without writing any tests and live to tell about it. Sure, you are probably aware of the fact that most of the time having meaningful unit tests would’ve made you faster long term, but you have real business value to add and it has to be finished by yesterday.
However, in a newly formed, constantly changing team of a few dozen people working on a massive project, located on 3 continents, spread across 12 time zones with different levels of experience and understanding of the task at hand yelling across the room, asking if someone knows should id be docID + “::” ID or just ID goes through the window. At that point, unit tests and integration tests become as necessary as working code itself, if not more.
In the pre-Docker era, you would usually add in-memory database such as H2, write mocks of other dependent services and call it a day. Thankfully today with Docker and Testcontainers things are much easier.
To quote authors of library themselves: “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.”
In order to start writing integration tests you just need to have Docker installed and write a few lines of code. Testcontainers takes care of container lifecycle, pulling images, waiting for container to be ready and much more.
Let’s dissect this code
App.class is bootstrap class of my app under test.
TestCouchbaseConfig.class is where Beans for test couchbase environment are defined, we’ll get back to it later.
Since I’m using Testcontainers to set up environment for Spring app it makes sense to dynamically initialize Spring context and that’s what CouchbaseInitializer class does. We could’ve used classpath resources to load properties from file, but I think this will suffice for the purpose of demo.
Next, the meat of these tests, @Testcontainers annotation.
This annotation enables Junit Jupiter integration with Testcontainers by scanning all fields in the annotated class for @Container annotation and then calls their container lifecycle methods. If the field is static, container will be reused for all tests in the class, while if the field is an instance field, container will be recreated for each test method in the class. That’s pretty much it for most of use cases. Rather simple, right?
After that, all that’s left is to configure CouchbaseEnvironment Bean to work with our containerized database. Since Testcontainers automatically finds free ports for our database we need to find mapped ports and configure CouchbaseEnvironment to use them. Here’s how:
Using this piece of code we’ve configured Spring Data Couchbase integration to use containerized Couchbase instance and we are ready to write integration tests. In each integration test you should extend AbstractCouchbaseTest class and then proceed to use Spring’s repositories and all other automagically configured stuff like you always did.
As software engineers, our main mission is to produce desired software at minimal cost possible. Technologies such as Docker and Testcontainer significantly lower the amount of effort required to implement modern DevOps practices, increasing the quality of our products while lowering the cost of developing them, thereby making them a valuable addition to toolchain of each software engineer. For a bigger picture here is a great overview of DevOps know-how.