Functional testing of console apps with Docker


For one of my apps I’d been manually testing some basic functions in a bunch of environments, and it was a huge pain. So I decided to automatize it. As a simplest solution I chose to run an environment in Docker and interact with them through pexpect.

First of all I tried to use docker-py, but it’s almost impossible to interact with app run in Docker container, started from docker-py with pexpect. So I just used Docker binary:

from contextlib import contextmanager
import subprocess
import shutil
from tempfile import mkdtemp
from pathlib import Path
import sys
import pexpect

# Absolute path to your source root:
root = str(Path(__file__).parent.parent.parent.resolve())


def _build_container(tag, dockerfile):
    """Creates a temporary folder with Dockerfile, builds an image and
    removes the folder.
    
    """
    tmpdir = mkdtemp()
    with Path(tmpdir).joinpath('Dockerfile').open('w') as file:
        file.write(dockerfile)
    if subprocess.call(['docker', 'build', '--tag={}'.format(tag), tmpdir],
                       cwd=root) != 0:
        raise Exception("Can't build a container")
    shutil.rmtree(tmpdir)


@contextmanager
def spawn(tag, dockerfile, cmd):
    """Yields spawn object for `cmd` ran inside a Docker container with an
    image build with `tag` and `dockerfile`. Source root is available in `/src`.
    
    """
    _build_container(tag, dockerfile)
    proc = pexpect.spawnu('docker run --volume {}:/src --tty=true '
                          '--interactive=true {} {}'.format(root, tag, cmd))
    proc.logfile = sys.stdout

    try:
        yield proc
    finally:
        proc.terminate()

_build_container is a bit tricky, but it’s because Docker binary can build an image only for file named Dockerfile.

This code can be used for running something inside a Docker container very simple, code for printing content of your source root inside the container will be:

with spawn(u'ubuntu-test', u'FROM ubuntu:latest', u'bash') as proc:
    proc.sendline(u'ls /src')

Back to testing, if we want to test that some application can print version, you can easily write py.test test like this:

container = (u'ubuntu-python', u'''
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -yy python
''')


def test_version():
    """Ensure that app can print current version."""
    tag, dockerfile = container
    with spawn(tag, dockerfile, u'bash') as proc:
        proc.sendline(u'cd /src')
        proc.sendline(u'pip install .')
        proc.sendline(u'app --version')
        # Checks that `version:` is in the output:
        assert proc.expect([pexpect.TIMEOUT, u'version:'])

You can notice the strange assert proc.expect([pexpect.TIMEOUT, u'version:']) construction, it works very simple, if there’s version: in output, expect returns 1, if timeout came first - 0.

Also you can notice that all strings are in unicode (u''), it’s for compatibility with Python 2. If you use only Python 3, you can remove all u''.

Examples.



comments powered by Disqus