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:
tomodachi_testcontainers.DockerContainer
- generic base class for all types of containers.tomodachi_testcontainers.WebContainer
- base class for web application containers.tomodachi_testcontainers.DatabaseContainer
- base class for relational database containers.
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.
The tomodachi_testcontainers.DockerContainer
forwards the container's logs to the pytest test output, which is useful
for debugging what happened inside the container.
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