7
\$\begingroup\$

I created a test that creates the docker image based on a Dockerfile, and test that the web app is running. I did it because sometimes during the development of this project, I broke the creation of the docker image. However, I would like for a review whether this is the best approach.

import subprocess
import time

import pytest
import requests
from testcontainers.core.container import DockerContainer
from testcontainers.core.wait_strategies import LogMessageWaitStrategy

port = 8000


@pytest.fixture(scope="module")
def app_container():
    # Build the docker image
    subprocess.run(["docker", "build", "-t", "my-app:test", "."], check=True)

    # Start the container
    # We need to provide necessary environment variables for the app to start
    container = DockerContainer("my-app:test")
    container.with_exposed_ports(port)
    container.with_env("PORT", str(port))
    container.with_env("DATABASE_CONNECTION_STRING", "sqlite:///:memory:")
    container.with_env("ENVIRONMENT", "test")

    container.waiting_for(LogMessageWaitStrategy("Uvicorn running on"))
    container.start()

    yield container

    container.stop()


@pytest.mark.timeout(20)
def test_actuators_info(app_container):
    host: str = app_container.get_container_host_ip()
    container_port: int = app_container.get_exposed_port(port)
    base_url = f"http://{host}:{container_port}"

    # Retry logic in case the app takes a moment to be fully responsive
    max_retries = 5
    for _ in range(max_retries):
        try:
            response = requests.get(f"{base_url}/actuators/info")
            if response.status_code == 200:
                assert response.json() is not None
                return
        except requests.exceptions.ConnectionError:
            pass
        time.sleep(0.5)

    pytest.fail("Could not connect to the application or received non-200 status")

\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

Magic numbers

It's a small note, but you have several magic numbers in your code.

For example, 200 is an HTTP response code indicating a successful request. Why not make your code more self-documenting with something like the following, leveraging the requests library you've already imported?

            if response.status_code == requests.codes['ok']:
                assert response.json() is not None
                return
\$\endgroup\$
5
  • 3
    \$\begingroup\$ requests.codes['ok'] can be shortened as requests.codes.ok \$\endgroup\$ Commented Jan 5 at 22:26
  • \$\begingroup\$ Another magic number is the image name "my-app:test" which is repeated in the code. \$\endgroup\$ Commented Jan 6 at 8:51
  • \$\begingroup\$ for some reason ty is complaining about using .codes.ok. \$\endgroup\$ Commented Jan 6 at 9:15
  • \$\begingroup\$ Isn't substituting 200 for requests.codes['ok'] just substituting a magic number for a magic string :/ ? (Why 'ok' and not 'Ok' or 'OK'?) \$\endgroup\$ Commented Jan 6 at 16:05
  • 2
    \$\begingroup\$ It provides a meaning. And should someone decide to break the entire internet and change the HTTP status codes, presumably the requests library author would change that and this code would still work.... while the rest of modern civilization collapses. \$\endgroup\$ Commented Jan 6 at 16:08
6
\$\begingroup\$

Retry

The retry mechanism can be implemented more flexibly by using urllib3 along with requests.

Example:

from urllib3.util import Retry
from requests import Session
from requests.adapters import HTTPAdapter

s = Session()
retries = Retry(
    total=3,
    backoff_factor=0.1,
    status_forcelist=[502, 503, 504],
    allowed_methods={'POST'},
)
s.mount('https://', HTTPAdapter(max_retries=retries))

Source: requests -Advanced Usage

Wait strategy

I am not really familiar with the testcontainers libs but I appreciate this opportunity to look into it, and review the relevant docs. I believe you could leverage the wait strategies like this, for example to wait for a port to become available:

container.waiting_for(PortWaitStrategy(port))

Actually, I believe you need CompositeWaitStrategy to handle multiple conditions that have to be satisfied. I haven't tested any of that though.

Validation

If possible, I would check more in depth that the JSON response contains a specific text or value we should expect to see on success. For all I know, the endpoint could return an error message of some sort, still in JSON format. Some APIs have a dedicated /health endpoint too.

Fixtures

Not really needed here but good to know: you can parametrize functions and fixtures. Something like this:

@pytest.fixture(scope="module", params=[8000])
def app_container():
    ...
    port = request.param

so you can run tests multiple times by parametrizing test values.

\$\endgroup\$
1
  • \$\begingroup\$ EXCELLENT feedback. Thanks! I will apply and let you know in a comment in case you're interested. \$\endgroup\$ Commented Jan 6 at 9:00

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.