Skip to content

Creating new Testcontainers

Creating a new Testcontainer is not harder than writing a docker run command. Testcontainers library API minimizes the amount of boilerplate code.

This guide will use the HTTPBin application as an example for creating new Testcontainers. HTTPBin is a simple HTTP request & response service; its Docker image tag at DockerHub is kennethreitz/httpbin.

Creating new Testcontainers with testcontainers-python library

You don't need to use Tomodachi Testcontainers if you don't want to. It's built on top of the testcontainers-python Python library, so that's all you need to start using Testcontainers.

The testcontainers-python provides a base class for defining Docker Containers - testcontainers.core.container.DockerContainer. New containers are created by defining a new class and inheriting from the DockerContainer base class.

Note

The testcontainers-python library has a lot of Testcontainer examples. To learn more about creating and configuring Testcontainers in Python, take a look at the library's source code.

from typing import Generator

import pytest
import requests
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs


class HTTPBinContainer(DockerContainer):
    def __init__(self, internal_port: int = 80, edge_port: int = 8080) -> None:
        super().__init__(image="kennethreitz/httpbin")
        self.with_bind_ports(internal_port, edge_port)

    def start(self, timeout: int = 60) -> "HTTPBinContainer":
        super().start()
        wait_for_logs(self, r"Listening at", timeout=timeout)
        return self


@pytest.fixture(scope="session")
def httpbin_container() -> Generator[HTTPBinContainer, None, None]:
    with HTTPBinContainer() as container:
        yield container


def test_httpbin_container_started(httpbin_container: HTTPBinContainer) -> None:
    base_url = f"http://localhost:{httpbin_container.get_exposed_port(80)}"

    response = requests.get(f"{base_url}/status/200", timeout=10)

    assert response.status_code == 200

It's necessary to override the start method and include a waiter function that checks that the container has started. The testcontainers-python provides a wait_for_logs utility for searching log patterns with regular expressions. To ensure that the HTTPBin container has started, we're waiting until the container emits the Listening at log. Container creation is an asynchronous process - it takes time for the application inside the container to start, so if we're not waiting until the app is ready and try to use it immediately, the tests will fail.

Creating new Testcontainers with tomodachi-testcontainers library

Tomodachi Testcontainers have a couple of base classes for defining new Testcontainers:

Using DockerContainer for generic containers

The tomodachi_testcontainers.DockerContainer base class inherits from testcontainers.core.container.DockerContainer. It adds some extra features - runs Testcontainers in a separate Docker network, forwards container logs to Python's standard logger, etc.

It's used in the same way as the testcontainers.core.container.DockerContainer, except it requires the definition of a method log_message_on_container_start; it logs a custom message when a container starts. It helps to access a Testcontainer while debugging. Since the container's port is selected randomly for every test run, it's helpful to output an HTTP URL to be able to open an app running in the container and interact with it.

from typing import Generator

import pytest
import requests
from testcontainers.core.waiting_utils import wait_for_logs

from tomodachi_testcontainers import DockerContainer
from tomodachi_testcontainers.utils import get_available_port


class HTTPBinContainer(DockerContainer):
    def __init__(self, internal_port: int = 80, edge_port: int | None = None) -> None:
        super().__init__(image="kennethreitz/httpbin")
        self.with_bind_ports(internal_port, edge_port or get_available_port())

    def log_message_on_container_start(self) -> str:
        return f"HTTPBin container: http://localhost:{self.get_exposed_port(80)}"

    def start(self, timeout: int = 60) -> "HTTPBinContainer":
        super().start()
        wait_for_logs(self, r"Listening at", timeout=timeout)
        return self


@pytest.fixture(scope="session")
def httpbin_container() -> Generator[DockerContainer, None, None]:
    with HTTPBinContainer() as container:
        yield container


def test_httpbin_container_started(httpbin_container: HTTPBinContainer) -> None:
    base_url = f"http://localhost:{httpbin_container.get_exposed_port(80)}"

    response = requests.get(f"{base_url}/status/200", timeout=10)

    assert response.status_code == 200

In the image below, the log_message_on_container_start outputs an HTTP URL to the debug console for accessing the container's app.

Debugging in VSCode

HTTPBin container URL is printed in the VSCode debug console (last row).

The tomodachi_testcontainers.DockerContainer forwards the container's logs to the pytest test output, which is useful for debugging what happened inside the container.

Container logs in pytest console

HTTPBin container logs are forwarded to pytest test output.

Using WebContainer for web application containers

Running web applications in containers is common, so tomodachi_testcontainers.WebContainer base class provides a convenient way to run web app containers.

It works out of the box for web apps that run on a single port. Simply provide the internal_port and edge_port, and WebContainer will bind them. If you need to bind more ports, bind each port separately with the with_bind_ports() method.

Note

If edge_port is left as None, a random available port on a host machine is selected with get_available_port().

The WebContainer also provides an optional http_healthcheck_path param similar to the Docker Healthcheck. To wait until a web app inside the container has started, the WebContainer will continuously request the application's http_healthcheck_path endpoint until it returns the HTTP 200 status code. This waiting strategy is more efficient and generic than waiting for the container's logs.

Lastly, the WebContainer provides helper methods, e.g., get_internal_url and get_external_url, for fetching the container's HTTP endpoints. See all methods in the code reference.

from typing import Generator

import pytest
import requests

from tomodachi_testcontainers import DockerContainer, WebContainer


class HTTPBinContainer(WebContainer):
    def __init__(self, internal_port: int = 80, edge_port: int | None = None) -> None:
        super().__init__(
            image="kennethreitz/httpbin",
            internal_port=internal_port,
            edge_port=edge_port,
            http_healthcheck_path="/status/200",
        )

    def log_message_on_container_start(self) -> str:
        return f"HTTPBin container: {self.get_external_url()}"


@pytest.fixture(scope="session")
def httpbin_container() -> Generator[DockerContainer, None, None]:
    with HTTPBinContainer() as container:
        yield container


def test_httpbin_container_started(httpbin_container: HTTPBinContainer) -> None:
    base_url = httpbin_container.get_external_url()

    response = requests.get(f"{base_url}/status/200", timeout=10)

    assert response.status_code == 200