Test beyond your code with docker & pytest
Tags: python, opensource, programming
Published: 2022-12-17 10:50:06

Docker opens up access to an incredible ecosystem of build-up applications. We find containers for everything … We can run a postgres database, run a redis database or launch an ftp server. 

Docker is a great way to run dependency our code needs when running system integration tests that will validate our application.

In this article, we will discover how to use docker in our python tests by writing a minimum of boilerplate while keeping the same CI/CD pipeline.

# Testing an application beyond its code thanks to docker

A bug can hide in the use of a database, for example, a badly written SQL query. We need a database to test that. We can’t validate this behavior with just the code.

It is an important topic. Django, a major python framework, provides a partial answer to this. It only manages a test workflow for the database. This workflow also requires you to launch the database by yourself before launching your tests.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xces1iidss4689592p95.jpg)

To solve this topic, our tests must be able to launch containers by themselves.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c9ekira8ho3jc03t7hmb.png)

To mount the containers from our tests, we will boost our tests with Fixtup. Our tests will be able to start and stop containers at the right time by themselves. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4zxp5oked67ygbsfxmql.png)

If a container is expensive to start, it can be mounted once and reused. pytest will stop it after playing the last test it has to play. 

# Install the requirements

To follow this article, we need to prepare our developper station.

1 . We need to be able to use docker and docker-compose from a terminal. 

```bash
$ docker --version
$ docker-compose --version
```

2 . We need poetry to create the python project

```bash
$ poetry
```

# Create a python project

```bash
$ poetry init
$ poetry add psycopg2-binary
$ poetry add --dev pytest fixtup
```

We will create a docker container for postgresql as a fixture to use in our tests.

```bash
$ poetry run fixtup init
Choose a directory to store fixture templates : tests/fixtures
Python manifest (pyproject.toml) [pyproject.toml]

$ poetry run fixtup new
Choose a fixture identifier : postgres
Mount environment variables on this fixture (y/n) [n]
Mount docker container on this fixture (y/n) [n] y
```

*tests/fixtures/postgres/docker-compose.yml*
```yaml
version: "3.9"
services:
    postgresql:
        init: true
        image: postgres
        ports:
            - "5432:5432"
        environment:
            - POSTGRES_PASSWORD=1234
        volumes:
            - /var/lib/postgresql
```

We will activate the hook_started hook to wait for the database server to be loaded before starting our test.

*tests/fixtures/postgres/.hooks/hook_started.py*
```python
from time import sleep
import fixtup. helper

fixtup.helper.wait_port(5432, timeout=2000)

# we wait 1 second to ensure the database is ready
sleep(1)
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kkd7ujc06cx7ffhvumpl.png)

# Using postgresql database in a test

Now we are going to install the driver for postgresql and write our first test.

*test/test_postgresql_database.py*
```python
import fixtup
import psycopg2

def test_postgres_should_work():
    with fixtup.up('postgres'):
        conn = psycopg2.connect("host=127.0.0.1 dbname=postgres user=postgres password=1234")
        core = conn.cursor()
        cur.execute('SELECT 1+1')
        res = cur.fetchall()
        assert res[0][0] == 2
        conn.close()
```

# Use the same postgresql database for all tests

Containers can take a long time to start, that’s why we would like to reuse them between several tests. By default, fixtup stops and starts them between each test. By activating the `keep_up` policy of fixtup, the containers will remain up as soon as they have been launched. They will be deleted when pytest has finished running.

*tests/fixtures/postgres/fixtup.yml*
```yaml
# This flag control if a fixture stay up and running between every test. The fixture is stop and
# unmount when the test process stop
#
# This attribute allow to start a database only once and stop the container only when unittest has finished to run
# the test suite. It may be interested to improve the performance if your start and stop process is too slow
keep_up: true
```

# Wait for database

Instead of waiting for port availability and a timer after to wait database to be ready, we will rewrite the hook_started to attempt to connect to the database regularly.

*tests/fixtures/postgres/.hooks/hooks_started.py*
```python
from time import monotonic
import psycopg2

start = monotonic()
connected = False
timeout = 5
while not connected:
    try:
        conn = psycopg2.connect("host=127.0.0.1 dbname=postgres user=postgres password=1234")
        connected = True
        conn.close()
    except Exception:
        if monotonic() - start > timeout:
            raise TimeoutError()
```

# Going further

In this article, we have seen how to use docker containers in our tests with pytest and [fixtup](https://pypi.org/project/fixtup/).

If your test depends on an application that works in docker, pytest can mount these containers through Fixtup. It can be a redis base, a localstack stack for AWS, or an ftp server...

If you're stepping through a test from pycharm or vscode, you can debug your test and also the fixture hooks.

We haven't seen how to mount the database schema at startup, nor how to clean up the database between each test. Fixtup hooks allow you to do this. 

I plan to develop a fixtup plugin for sqlalchymie and another for django that will allow you to use a database without writing a specific hook.


* [Fixtup](https://github.com/FabienArcellier/fixtup)
* [Fixtup - Documentation](https://fixtup.readthedocs.io/en/latest/)
* [Fixtup - Discord](https://discord.gg/nMn9YPRGSY)