このエントリでは、
Docker Composeを使って、
テスト対象の起動からテスト実行までをまとめて実行したい時に、
必要になりそうなノウハウを取り上げます。

ここでは、次のような要件があると考えました。

  • 開発環境構築用docker-compose.yamlはそのまま変更せず、テスト実行用のコンテナを追加したい
  • テストの成否によってDocker Compose実行後の終了コードを変えたい
  • 異常系テストのために一部コンテナを停止するなど、コンテナをテストコードから制御したい
  • Docker Composeを用いたテストをGitHub Actionsで実行したい

テスト対象のdocker-compose.yamlを用意

まずはテスト対象となる環境を作ります。
ここでは、nginxが動いているだけのシンプルなdocker-compose.yamlを用意します。

ファイル構成

次のようなファイル構成にします。

  • docker-compose.yaml

各ファイルの内容は次の通りです。

docker-compose.yaml

version: "3.9"
services:
  app:
    image: "nginx:latest"
    ports:
      - 8080:80
    hostname: app
    networks:
      - app-network
networks:
  app-network:

環境の起動

次のようにパラメータを指定して、環境を起動します。

docker compose up

http://localhost:8080/ にアクセスし、nginxが動いていることを確認します。

以降の説明で、この環境に対してテストを追加していきます。

開発環境構築用docker-compose.yamlはそのまま変更せず、テスト実行用のコンテナを追加したい

この要件に対しては、次のMultiple Compose filesを利用できます。

Share Compose configurations between files and projects | docker docs
https://docs.docker.com/compose/extends/

docker-compose.yaml に開発環境用の構成が書かれているとして、
docker-compose.test.yaml up にテスト用コンテナを追加・テストに必要な開発環境の設定の上書きを記載すれば、
次のように実行すれば、開発環境用構成にテスト用構成を追加してテストを実行できます。

docker compose -f docker-compose.yaml -f docker-compose.test.yaml up

対象環境が起動してからテストを実行したいので、healthyとdepends_onも指定しておきます。

healthcheck | Compose ファイル version 3 リファレンス
https://docs.docker.jp/compose/compose-file/compose-file-v3.html#healthcheck

depends_on | Compose ファイル version 3 リファレンス
https://docs.docker.jp/compose/compose-file/compose-file-v3.html#depends-on

ファイル構成

次のようなファイル構成にします。

  • docker-compose.yaml
  • docker-compose.test.yaml ★追加
  • test
    • Dockerfile ★追加
    • .dockerignore ★追加
    • requirements.txt ★追加
    • tests
      • test_study.py ★追加

各ファイルの内容は次の通りです。

docker-compose.test.yaml

version: "3.9"
services:
  test:
    build: test/
    command: python -m pytest
    depends_on:
      app:
        condition: service_healthy
    networks:
      - app-network
  app:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]

test/Dockerfile

FROM "python:3.10.11"

WORKDIR /test
COPY requirements.txt /test/
RUN pip install -r requirements.txt

COPY . /test

CMD ["python", "-m", "pytest"]

test/.dockerignore

venv

test/requirements.txt

pytest==7.3.1
requests==2.30.0

test/tests/test_study.py

import requests

def test_study():
    r=requests.get("http://app/", timeout=(1,1))
    assert "nginx" in r.text

テスト実行

次のようにパラメータを指定して、テストを実行します。

docker compose -f docker-compose.yaml -f docker-compose.test.yaml up --build

テスト結果が、以下のように表示されます。

multi-test-1  | ============================= test session starts ==============================
multi-test-1  | platform linux -- Python 3.10.11, pytest-7.3.1, pluggy-1.0.0
multi-test-1  | rootdir: /test
multi-test-1  | collected 1 item
multi-test-1  |
multi-app-1   | ***.***.***.*** - - [14/May/2023:18:20:08 +0000] "GET / HTTP/1.1" 200 615 "-" "python-requests/2.30.0" "-"
multi-test-1  | tests/test_study.py .                                                    [100%]
multi-test-1  |
multi-test-1  | ============================== 1 passed in 0.05s ===============================

この段階では、テスト実行後もdocker-composeが実行されたままになります。
その対応については、次の項目で説明します。

テストの成否によってDocker Compose実行後の終了コードを変えたい

この要件に対しては、docker compose upの--exit-code-fromが利用できます。
--abort-on-container-exitも利用しますが、exit-code-fromで暗黙的に指定されます。

docker-compose up | Docker-docs-ja
https://docs.docker.jp/compose/reference/up.html

テストの実行

次のようにパラメータを指定して、テストを実行します。

docker compose -f docker-compose.yaml -f docker-compose.test.yaml up --build --exit-code-from test

テスト成功時と失敗時で、次のように終了コードがかわります。

成功時

$ docker compose -f docker-compose.yaml -f docker-compose.test.yaml up --build --exit-code-from test
※省略※
multi-test-1  | ============================= test session starts ==============================
multi-test-1  | platform linux -- Python 3.10.11, pytest-7.3.1, pluggy-1.0.0
multi-test-1  | rootdir: /test
multi-test-1  | collected 1 item
multi-test-1  |
multi-app-1   | ***.***.***.*** - - [14/May/2023:18:24:13 +0000] "GET / HTTP/1.1" 200 615 "-" "python-requests/2.30.0" "-"
multi-test-1  | tests/test_study.py .                                                    [100%]
multi-test-1  |
multi-test-1  | ============================== 1 passed in 0.05s ===============================
※省略※
$ echo $?
0

失敗時

$ docker compose -f docker-compose.yaml -f docker-compose.test.yaml up --build --exit-code-from test
※省略※
multi-test-1  | ============================= test session starts ==============================
multi-test-1  | platform linux -- Python 3.10.11, pytest-7.3.1, pluggy-1.0.0
multi-test-1  | rootdir: /test
multi-test-1  | collected 1 item
multi-test-1  |
multi-app-1   | ***.***.***.*** - - [14/May/2023:18:25:12 +0000] "GET / HTTP/1.1" 200 615 "-" "python-requests/2.30.0" "-"
multi-test-1  | tests/test_study.py F                                                    [100%]
multi-test-1  |
multi-test-1  | =================================== FAILURES ===================================
multi-test-1  | __________________________________ test_study __________________________________
multi-test-1  |
multi-test-1  |     def test_study():
multi-test-1  |         r=requests.get("http://app/")
multi-test-1  | >       assert "apache" in r.text
multi-test-1  | E       assert 'apache' in ※省略※
multi-test-1  |
multi-test-1  | tests/test_study.py:5: AssertionError
multi-test-1  | =========================== short test summary info ============================
multi-test-1  | FAILED tests/test_study.py::test_study - assert 'apache' in '<!DOCTYPE html>\...
multi-test-1  | ============================== 1 failed in 0.06s ===============================
multi-test-1 exited with code 1
※省略※
$ echo $?
1

以上のように終了コードで成功判定できるので、CIにも組み込みやすくなります。

異常系テストのために一部コンテナを停止するなど、コンテナをテストコードから制御したい

この要件を満たすためには、
コンテナ内で動いているコードからDockerコマンドやAPIを利用出来るようにする必要があります。
これはDooD(Docker outside of Docker)と呼ばれている方法で実現する事ができます。
ホストの/var/run/docker.sockをコンテナから参照すればOKです。

参考: dind(docker-in-docker)とdood(docker-outside-of-docker)でコンテナを料理する | qiita https://qiita.com/t_katsumura/items/d5be6afadd6ec6545a9d

また、コンテナを操作するためには、対象のコンテナを判別する必要があります。
docker-composeで指定したサービス名からコンテナIDを特定するには、
dockerコンテナのラベルを使います。
以下の2つのラベルを利用します。

  • com.docker.compose.service: サービス名
  • com.docker.compose.project: プロジェクト名

Services top-level element | docker-docs
https://docs.docker.com/compose/compose-file/05-services/

ファイル構成

次のようなファイル構成にします。

  • docker-compose.yaml
  • docker-compose.test.yaml ★更新
  • test
    • Dockerfile
    • .dockerignore
    • requirements.txt ★更新
    • tests
      • test_study.py ★更新

各ファイルの内容は次の通りです。

docker-compose.test.yaml

version: "3.9"
services:
  test:
    build: test/
    command: python -m pytest
    depends_on:
      app:
        condition: service_healthy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - app-network
  app:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost"]

test/requirements.txt

pytest==7.3.1
requests==2.30.0
docker==6.1.2

test/tests/test_study.py

import pytest
import requests
from requests.exceptions import Timeout
import docker

def test_study():
    client: docker.DockerClient = docker.from_env()
    project="※プロジェクト名に置き換え※"
    service="app"
    containers = [
        c.id
        for c
        in client.containers.list(filters={
            'label': [f"com.docker.compose.project={project}", f"com.docker.compose.service={service}"],
            'status': 'running'
        })
    ]

    # pause containers
    for c in containers:
        client.api.pause(c)

    with pytest.raises(Timeout) as e:
        r=requests.get("http://app/", timeout=(1,1))

    # unpause containers
    for c in containers:
        client.api.unpause(c)

テストの実行

テストを実行すると、次のように結果が出力されます。

$ docker compose -f docker-compose.yaml -f docker-compose.test.yaml up --build --exit-code-from test
※省略※
multi-test-1  | ============================= test session starts ==============================
multi-test-1  | platform linux -- Python 3.10.11, pytest-7.3.1, pluggy-1.0.0
multi-test-1  | rootdir: /test
multi-test-1  | collected 1 item
multi-test-1  |
multi-app-1   | ***.***.***.*** - - [14/May/2023:19:19:43 +0000] "GET / HTTP/1.1" 200 0 "-" "python-requests/2.30.0" "-"
multi-test-1  | tests/test_study.py .                                                    [100%]
multi-test-1  |
multi-test-1  | ============================== 1 passed in 1.12s ===============================
multi-test-1 exited with code 0
※省略※

Docker Composeを用いたテストをGitHub Actionsで実行したい (2023.05.21追記)

最後に、これらをGitHub Actionsから実行します。

ここまでの手順でもろもろの準備はできているので、 あとは、GitHub Actionsのワークフローでdocker composeを実行するだけです。

ファイル構成

次のようなファイル構成にします。

  • docker-compose.yaml
  • docker-compose.test.yaml
  • test
    • Dockerfile
    • .dockerignore
    • requirements.txt
    • tests
      • test_study.py
  • .github
    • workflows
      • e2etest.yaml

各ファイルの内容は次の通りです。

.github/workflows/e2etest.yaml

name: e2etest
on: [push]

jobs:
  unit_test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: test
        run: docker compose -f docker-compose.yaml -f docker-compose.test.yaml up --build --exit-code-from test

以上。