What is Testcontainers?
Testcontainers is a popular open-source library that allows developers to run lightweight, throwaway containers for integration testing. It provides real dependencies, such as databases, message brokers, and browsers, inside Docker containers, ensuring consistency across different environments.
My Experience with Testcontainers
As a test automation engineer, I have used Testcontainers in a TypeScript project with Selenium and Cucumber to improve test reliability and streamline execution in a Bitbucket CI/CD pipeline. It allowed me to run browser tests inside a disposable, isolated environment without worrying about local setup inconsistencies.
- By leveraging Testcontainers for Selenium, I could:
- Run Chrome browser tests in a containerized environment.
- Ensure a clean, fresh browser instance for each test run.
- Record executions as MP4 videos for debugging test failures.
- Easily integrate with a CI/CD pipeline to maintain consistency across different environments.
Now, let’s dive into how Testcontainers works and why it's a game-changer for integration testing.
Why QA Engineers Love Testcontainers
Testcontainers provides a robust, real-world testing environment that eliminates unreliable test setups. Instead of using mocks or in-memory solutions, QA engineers can leverage real services running in isolated containers.
Key benefits include:
- Stable Test Environments: Ensures every test starts with a clean, isolated state, reducing flaky test failures.
- Testing with Real Dependencies: Instead of mocks, Testcontainers provides actual databases, message brokers, and browser instances.
- Cross-Browser UI Testing: Supports headless and full-browser Selenium tests, ensuring compatibility across environments.
- Seamless CI/CD Integration: Runs smoothly in Bitbucket Pipelines, GitHub Actions, and GitLab CI/CD.
- MP4 Video Recording for Debugging: With .withRecording(), every browser session can be recorded, helping teams debug failures visually.
- Faster Test Feedback with Wait Strategies: Ensures containers fully initialize before tests start, preventing race conditions.
- Customizable Environments: QA teams can define test environments using GenericContainer or build custom Dockerfile-based containers.
With Testcontainers, QA teams can run more accurate, reliable, and efficient tests that closely mimic production conditions. 🚀
Types of Containers Supported
- Databases: PostgreSQL, MySQL, MongoDB, Redis, etc.
- Message Brokers: Kafka, RabbitMQ
- Selenium: Running headless browsers for UI testing
- Custom Containers: Define your own images and configurations
Wait Strategies
Testcontainers offers several Wait Strategies to ensure that a container is ready before interacting with it. These strategies help avoid race conditions where a test might attempt to use a container before it has fully initialized. Some common wait strategies include:
- Wait.forLogMessage(regex, times) – Waits for a specific log message to appear a certain number of times.
- Wait.forHealthCheck() – Waits for a container’s health check to return a healthy status.
- Wait.forListeningPorts(port) – Ensures that a specific port inside the container is open before proceeding.
- Wait.forHttp(path, statusCode) – Waits for an HTTP endpoint to respond with a specific status code.
Example of using a wait strategy in Testcontainers for a PostgreSQL container:
import { GenericContainer, Wait } from "testcontainers";
const container = await new GenericContainer("postgres:latest")
.withExposedPorts(5432)
.withEnv("POSTGRES_USER", "testuser")
.withEnv("POSTGRES_PASSWORD", "testpassword")
.withWaitStrategy(Wait.forLogMessage("database system is ready to accept connections", 2))
.start();
Using wait strategies ensures that the container is fully initialized before the test suite begins execution, reducing flakiness in tests.
Building Your Own Images
In addition to using pre-built container images, Testcontainers allows you to build and use custom container images tailored to your test needs. You can create a custom image using GenericContainer
and specify a Dockerfile or existing image:
import { GenericContainer } from "testcontainers";
const container = await new GenericContainer("node:latest")
.withCopyFileToContainer("./my-app", "/usr/src/app")
.withExposedPorts(3000)
.withCommand(["npm", "start"])
.start();
console.log(`Server running at http://localhost:${container.getMappedPort(3000)}`);
This approach is useful when testing applications that require custom dependencies or specific configurations. You can also build an image from a Dockerfile dynamically using withBuild:
const container = await GenericContainer.fromDockerfile("./path/to/Dockerfile").build();
Using Testcontainers in TypeScript
Below is an example of using Testcontainers with Selenium in a TypeScript project for running UI tests in a CI/CD pipeline (e.g., Bitbucket).
Prerequisites
- Docker installed on your machine or CI environment.
- Node.js and TypeScript installed.
- Selenium WebDriver dependencies (selenium-webdriver, @testcontainers/selenium, dotenv, etc.).
Setting Up Testcontainers for Selenium
import * as dotenv from 'dotenv';
import { After, AfterAll, Before, BeforeAll, setDefaultTimeout, Status } from '@cucumber/cucumber';
import { Browser, Builder } from 'selenium-webdriver';
import * as chrome from 'selenium-webdriver/chrome';
import { SeleniumContainer } from '@testcontainers/selenium';
setDefaultTimeout(300000);
export let browser: any;
let container: any;
BeforeAll(async () => {
dotenv.config();
container = await new SeleniumContainer('seleniarm/standalone-chromium:latest')
.withRecording() // Enables video recording of the test execution
.start();
});
Before(async () => {
const chromeOptions = new chrome.Options()
.addArguments('--start-maximized')
.addArguments('--disable-notifications')
.addArguments('--no-sandbox')
.addArguments('--headless');
browser = await new Builder()
.forBrowser(Browser.CHROME)
.setChromeOptions(chromeOptions)
.usingServer(container.getServerUrl())
.build();
await browser.manage().setTimeouts({ implicit: 30000, pageLoad: 120000, script: 30000 });
await browser.get('https://example.com/login');
await browser.manage().deleteAllCookies();
});
After(async function (testCase) {
if (testCase.result?.status === Status.FAILED) {
const image = await browser.takeScreenshot();
this.attach(image, 'base64:image/png');
}
await browser.quit();
});
AfterAll(async () => {
const stoppedContainer = await container.stop();
await stoppedContainer.saveRecording('./image/lastExecution.mp4'); // Saves the execution video
});
Running Testcontainers in a CI/CD Pipeline
Testcontainers integrates seamlessly into Bitbucket Pipelines (or other CI/CD environments) to ensure consistent test execution across different environments. Below is a sample bitbucket-pipelines.yml configuration for running Selenium tests:
image: node:latest
pipelines:
default:
- step:
name: Run Selenium Tests with Testcontainers
services:
- docker
script:
- npm install
- npm test
This setup ensures that your tests run inside a Docker-enabled pipeline, leveraging Testcontainers for reliable and isolated test execution.
Conclusion
Testcontainers is a powerful tool that simplifies integration testing by providing real, disposable environments. Whether you're testing databases, message queues, or Selenium-based UI tests, it ensures that tests are reliable and consistent across different environments. Additionally, video recordings of test execution allow for easy debugging and verification, making it a valuable tool for both local development and CI/CD pipelines.
For even more flexibility, building your own images with GenericContainer
or fromDockerfile
enables custom test environments tailored to your application's needs. Using Wait Strategies ensures that containers are fully initialized before execution, reducing flakiness and improving test stability.
Top comments (0)