Emulating real dependencies in Integration Tests using Testcontainers
A hands-on guide on Integration Tests in Go using Testcontainers.
What is Integration Testing?
Contrary to unit testing, the purpose of integration tests is to validate that different software components, subsystems or applications work well together combined as a group.
It’s a very important step in the testing pyramid, that can help to identify the issues that arise when the components are combined, for example compatibility issues, data inconsistence, communication issues.
In this article, we define the integrations tests as tests of communication between our backend application and external components such as database and cache.
Different ways of running the Integration Tests

While unit tests are easy to run (you just execute tests as you would execute your code), integration tests usually require some scaffolding. In the companies I worked I’ve seen the following approaches to address the integration testing environment problem.
Option 1. Using the throwaway databases and other dependencies, which must be provisioned before the integration tests start and destroyed afterwards. Depending on your application complexity the effort of this option can be quite high, as you must ensure that the infrastructure is up and running and data is pre-configured in a specific desired state.
Option 2. Using the existing shared databases and other dependencies. You may create a separate environment for integration tests or even use the existing one (staging for example) that integration tests can use. But there are many disadvantages here, and I would not recommend it. Because it is a shared environment, multiple tests can run in parallel and modify the data simultaneously, therefore you may end up with inconsistent data state for multiple reasons.
Option 3. Using in-memory or embedded variations of the required services for integration testing. While this is a good approach, not all dependencies have in-memory variations, and even if they do, these implementations may not have the same features as your production database.
Option 4. Using Testcontainers to bootstrap and manage your testing dependencies right inside your testing code. This ensures a full isolation between test runs, reproducibility and better CI experience. We will dive into that in a second.
Our Guinea Pig Service: dead simple URL Shortener
Do demonstrate the tests we prepared a dead simple URL shortener API written in Go, which uses MongoDB as a data storage and Redis as a read-through cache. It has two endpoints which we’ll be testing in our tests:
/create?url= generates the hash for a given URL, stores it in database.
/get?key= returns the original URL for a given key.



