Dockerizing Python Test Environments
Apr 4, 2017
Eli Uriegas
4 minute read

Context

Failing pull requests

It’s every maintainer’s nightmare to see these types of failures on PR’s. Nothing in the code base had changed and tests were passing locally, but almost every build we pushed through was failing. I spent so long trying to figure out why they were failing on Travis CI but not on my local machine, but could not find any reason why it was actually happening. Then, I did what any developer would do in a similar situation: I spun up a virtual machine close to what Travis CI would be like and tried to recreate the steps through there.

Virtual Machine Troubles

I’ve been using virtual machines for a while (yay for having a Windows machine in college) so I would say I am pretty familiar with them. My favorite virtual machine management tool is VMware Workstation and it’s fairly simple to create a new machine, but the time it takes to do this can be costly. The time taken is negligible if the machine is supposed to be persistent (like an ubuntu box for school) but for use cases like needing consistent testing environments it may prove to be a bit longstanding. So being the studious developer I am I decided now is a great time to learn how to do this in a more automated fashion. In comes Docker!

How can Docker help?

With Docker we can be assured everyone uses the same base image and that the only things done in the container are the things we prescribe (as specified in a Dockerfile). We can also use a Makefile to make it easier for users to run a single command instead of maybe two or three commands. Execution (when including the creation of the Docker image) may take longer but having the consistency of testing environments is invaluable when trying to run unittests.

How do we implement it?

Implementation of this type of unittesting was fairly trivial requiring only about eight lines of real code.

The Dockerfile

FROM python:3.6

ADD . /app
WORKDIR /app

RUN pip install tox

This file will tell Docker to build an image from the base image python:3.6 which includes python3.6 obviously and all the tools necessary to compile CPython extensions as well. It will also add our current application code as a volume on the container and then change our working directory to that application folder. Finally it will install tox on the container and prep it for running actual tests.

The Makefile

test:
    # Remove all cached pyc files, they don't play nice with the containers
    find . -name "*.pyc" -delete
    # Build the docker image
    docker build -t sanic/test-image .
    # Run `tox` on the image
    docker run -t sanic/test-image tox

The makefile combines 3 commands to make running the unittests possible. The first removes all pyc files contained in the application folder so that they don’t interfere when we try to run tests inside of the container. Next is the command that actually builds the test image using the Dockerfile. And last is the command that runs tox on the container which will show us how results of actually running the tests.

So what are the drawbacks?

So this approach is good for our use case but recreating images creates a lot of cruft after so many runs in the form of untagged images. Also a lot of exited containers are created as well which becomes really unsightly after running docker ps -a and seeing a bunch of exited containers.

How do we clean it up?

A lot of blog posts have been written about how to clean up exited containers as well as untagged images with my favorite being one by Jim Hoskins. Just in case you don’t want to read it though here are the commands.

For cleaning containers:

docker rm $(docker ps -a -q)

For cleaning images:

docker rmi $(docker images | grep "^<none>" | awk "{print $3}")

Conclusion

Dockerizing environments for use with python testing is useful for those who dev on multiple different machines. It allows us to have a consistent testing environment that is nearly guaranteed to be the same almost every single time. Most importantly, it’s easy. The ramp up time to get this type of workflow going is minimal so why wouldn’t you be doing it?