Docker Demos — Running unit tests in Docker Compose


Hi, friend! Welcome back, and thank you for learning along with me. In our last tutorial, we learned how to build a Docker container and simultaneously run a Bash file within the container to iteratively call a Python script. But what if the Python script is some sort of analysis tool? Following good programming etiquette, we should also write a unit test for our analysis functions. Today, we’ll see how to merge these two concepts: Docker containers and programming unit tests.

The Setup

The main idea behind the setup we’ll be building is this:

  • Build a single Docker container; the container’s image should have Python installed.
  • The container will have a /smee directory which will house two new Python files: and
  • The container will be built using Docker and Docker Compose.
  • The file will simply print a message (“Hello, Python user!”).
  • The file will test that the output from is equal to itself.

Conceptually, the logic is pretty simple, but the Python files can be a bit nonintuitive. In the interest of transparency, our folder hierarchy will look like something like this. We have a folder (“docker_intro”) housing a Docker file (dockerfile), a Compose file (docker-compose.yml), and a subdirectory titled smee. The smee folder contains two new Python files: and Now we need to describe the the contents of each file!

Docker file

Since we still need Python within our container’s image, we’ll keep the previous Python image, 3.10-slim-bullseye build. The AS base portion of the image call defines our image as part of the build stage. In doing so, base becomes a label we can use later in the Docker file to call and work from.

We also need to copy files from our host directory into the container. So, we transition into the work directory /smee in our container and copy everything from our current host folder into the working directory. Finally, we define our base stage as our test stage. Within the test stage of our Docker file, we issue a command within the container to run the file located within the /smee directory using the python command.

# Use the Python image, define as the "base" stage
FROM python:3.10-slim-bullseye AS base

# Change to container directory. Copy over files.
COPY . .

# Define "base" stage as "test" stage. Run
FROM base as test
CMD ["python", "./smee/"]

Compose file

Our Compose file is pretty similar to our previous tutorials. We’re running Compose version 3.9, and we only have a single service, container. The container service builds a container called first_container using our Docker file titled dockerfile.

version: '3.9'

# Define services
        container_name: first_container
            context: .
            dockerfile: dockerfile

Python files:

As we noted earlier, the file is very basic. We define a function, hello_world, that simply returns a string, “Hello, Python user!”. We then call hello_world from our main function (main()), defining the output of our first function to the variable message. A print statement ensures the hello_world function is being called properly. Finally, we set our main function to run automatically when the script is executed.

import sys

def hello_world():
    return "Hello, Python user!"

def main(n):
    message = hello_world()

if __name__ == '__main__':

Python files:

The script is a bit more complex, but it’s functionality is straight forward. We begin by importing two things: (1) the unit test module (named unittest), and (2) our hello_world function from our script. The former lets us access Python’s suite of unit test capabilities, while the latter allows us to access the function that generates our message.

We then define a class to test our message — we title this TestMessage, which ingests the TestCase method from the Python’s unittest module. This is the basic test class. Within the test class, we define our function to test the message itself; we call this test_message.

This is the actual test we wanted to run within our container. We define a message variable, which will store the output of the hello_world function from our helloworld script. We then ask Python to assert if the string contained in our message variable is equal to the same phrase we asked it to print, “Hello, Python user!”. If the two messages are equal, the test will pass. Similar to our other Python script, we then set the unit test to run automatically when our script is run.

import unittest
from helloworld import hello_world

class TestMessage(unittest.TestCase):
    Basic test class

    def test_message(self):
        The actual test.
        message = hello_world()
        self.assertEqual(message,"Hello, Python user!")

if __name__ == '__main__':

Putting it all together: Docker

So now we’ve defined our two Python files and our Docker file. Similar to our previous tutorial on building Docker containers in VS Code, we’ll tell Docker to build our container. While doing so, however, we’re doing something we haven’t done before: we’re targeting a specific stage of our build using the --target flag. Specifically, we’re asking Docker to build the test stage of our build, but the test stage is built on our base stage, which includes the Python image. We’re also giving the resulting container a label (or tag) of test:v0 with the -t flag.

vscode ➜ /com.docker $ docker build --target test -t "test:v0" .
[+] Building 0.6s (8/8) FINISHED                                                                                                                                                                           
 => [internal] load build definition from Dockerfile                                                                                                                                                  0.0s
 => => transferring dockerfile: 38B                                                                                                                                                                   0.0s
 => [internal] load .dockerignore                                                                                                                                                                     0.0s
 => => transferring context: 2B                                                                                                                                                                       0.0s
 => [internal] load metadata for                                                                                                                          0.5s
 => [base 1/3] FROM                                                               0.0s
 => [internal] load build context                                                                                                                                                                     0.0s
 => => transferring context: 321B                                                                                                                                                                     0.0s
 => CACHED [base 2/3] WORKDIR /semm                                                                                                                                                                   0.0s
 => CACHED [base 3/3] COPY . .                                                                                                                                                                        0.0s
 => exporting to image                                                                                                                                                                                0.0s
 => => exporting layers                                                                                                                                                                               0.0s
 => => writing image sha256:6ab866cad69a8ad4e2e86177511938b7ca7f7a0d0d7c3ea77e36799eabf29cbc                                                                                                          0.0s
 => => naming to    

Perfect! So now we have a container, and we can validate the container’s existence with the docker container ls or docker ps command. More importantly, we can interactively run the container we just built using docker run -it test:v0. WHen we do this, we get an output resembling something we’d expect to see from a Python output during unit tests:

vscode ➜ /com.docker $ docker run -it test:v0
Ran 1 test in 0.000s


Neat! That works great — our unit test is called correctly and our test passes. Next, let’s try to build the container with Compose. We do this by simply calling the up command in Compose: docker-compose run --build. Now let’s look at the result:

# Run Compose, ensure container is rebuilt
vscode ➜ /com.docker $ docker-compose up --build
Building container
[+] Building 1.4s (8/8) FINISHED                                                                                                                                                                           
 => [internal] load build definition from dockerfile                                                                                                                                                  0.0s
 => => transferring dockerfile: 197B                                                                                                                                                                  0.0s
 => [internal] load .dockerignore                                                                                                                                                                     0.0s
 => => transferring context: 2B                                                                                                                                                                       0.0s
 => [internal] load metadata for                                                                                                                          1.2s
 => [base 1/3] FROM                                                               0.0s
 => [internal] load build context                                                                                                                                                                     0.1s
 => => transferring context: 2.88kB                                                                                                                                                                   0.0s
 => CACHED [base 2/3] WORKDIR /semm                                                                                                                                                                   0.0s
 => [base 3/3] COPY . .                                                                                                                                                                               0.1s
 => exporting to image                                                                                                                                                                                0.0s
 => => exporting layers                                                                                                                                                                               0.0s
 => => writing image sha256:743446fbb44d35a4af51dbb8fa13418fdeb72bf3fd0567715ab4c21cdda48b22                                                                                                          0.0s
 => => naming to                                                                                                                             0.0s

Creating first_container ... done
Attaching to first_container
first_container | .
first_container | ----------------------------------------------------------------------
first_container | Ran 1 test in 0.000s
first_container | 
first_container | OK
first_container exited with code 0

Fantastic! Not only did our container build, but during the build our Python scripts ran as requested without us having to issue a specific command in Compose. Now that we are able to make unit tests that automatically run for our Python scripts, we can begin to ensure the robustness of our code, and thus our application as a whole.


In this tutorial, we’ve constructed to basic Python scripts — one Python script to print a “Hello world” message, and the other to call that script and run a test on its output. Then we built this capability into a single container. We built and ran the Docker container in both Docker and Compose, demonstrating that the unit tests ran successfully in both cases. In doing so, we gave ourselves a huge hand up in making our code more robust!

Next, join me for a quick demo of how to install a Conda environment inside a Docker container! Until then, thanks again for learning with me — we’re all in this together! If you’re enjoying the content, please feel free to Like, comment, and subscribe!

Get new content delivered directly to your inbox.

(Header image: Wave border by

%d bloggers like this: