Dockerでコンテナ化されたWebアプリケーションに対して、
システム時間を変化させたテストを実行したかったので、手順をまとめました。

ここでは、libfaketimeというものを使います。

libfaketime | GitHub
https://github.com/wolfcw/libfaketime

対象のWebアプリケーション

テスト対象のWebアプリは、次の仕様とします。

  • 土曜日・日曜日の時: Hello, Holiday! と表示
  • それ以外: Hello, Working! と表示

以降に、構成と実行の流れを示します。

対象Webアプリの構成

ディレクトリ構成と各ファイルは次の通り。

ディレクトリ構成

  • docker-compose.yaml
  • app
    • main.py
    • requirements.txt
    • Dockerfile

各ファイルの内容

app/main.py

import datetime
from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    if datetime.datetime.now().weekday() in [5, 6]:
        return "<p>Hello, Holiday!</p>"
    return f"<p>Hello, Working!</p>"

if __name__ == "__main__":
    app.run(host="0.0.0.0")

app/requirements.txt

Flask==2.2.3

app/Dockerfile

FROM python:3.10.10

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

CMD ["python", "main.py"]

docker-compose.yaml

services:
  app:
    build:
      context: app
    ports:
      - 5000:5000

対象Webアプリの実行

Webアプリを起動する。

docker compose up --build

Webアプリにアクセスする。

$ curl http://localhost:5000/
<p>Hello, Working!</p>

※土日であれば、Hello, Holiday!が返却される。

対象のWebアプリにLibfaketimeを追加する

テスト対象のWebアプリにlibfaketimeを追加します。

対象のWebアプリのイメージやdocker-composeを変更したくないので、
ライブラリのbuild用のコンテナを追加します。

以降に、追加するファイルと実行の流れを示します。

構成に追加するファイル

追加するファイルは次の通り。

ディレクトリ構成

  • docker-compose.yaml
  • docker-compose.libfaketime.yaml ★追加
  • app
    • main.py
    • requirements.txt
    • Dockerfile
  • libfaketime
    • Dockerfile ★追加

各ファイルの内容

docker-compose.libfaketime.yaml

services:
  app:
    depends_on:
      - libfaketime-build
    environment:
      - LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1
    volumes:
      - libfaketime:/usr/local/lib/faketime
  libfaketime-build:
    build:
      context: libfaketime
    command: cp -f /usr/local/lib/faketime/libfaketime.so.1 /mnt
    volumes:
      - libfaketime:/mnt
volumes:
  libfaketime:

libfaketime/Dockerfile

FROM python:3.10.10

WORKDIR /
RUN apt-get update \
  && apt-get install -y wget build-essential \
  && wget https://github.com/wolfcw/libfaketime/archive/refs/tags/v0.9.10.tar.gz \
  && tar zxf v0.9.10.tar.gz \
  && cd libfaketime-0.9.10/ \
  && make install \
  && cd ../ \
  && rm -rf libfaketime-0.9.10 \
  && rm v0.9.10.tar.gz

VOLUME /usr/local/lib/faketime

libfaketimeを適用してアプリを実行する

libfaketimeをロードしてWebアプリを起動する。

docker compose -f docker-compose.yaml -f docker-compose.libfaketime.yaml up -d --build

Webアプリのコンテナを停止、FAKETIMEを指定して起動する。(平日の場合)

docker compose -f docker-compose.yaml -f docker-compose.libfaketime.yaml stop app
docker compose -f docker-compose.yaml -f docker-compose.libfaketime.yaml run -d -e "FAKETIME=2023-03-06 00:00:00" --service-ports app
$ curl http://localhost:5000/
<p>Hello, Working!</p>

Webアプリのコンテナを停止、FAKETIMEを指定して起動する。(土日の場合)

docker compose -f docker-compose.yaml -f docker-compose.libfaketime.yaml stop app
docker compose -f docker-compose.yaml -f docker-compose.libfaketime.yaml run -d -e "FAKETIME=2023-03-12 00:00:00" --service-ports app
$ curl http://localhost:5000/
<p>Hello, Holiday!</p>

コンテナを終了する。

docker compose -f docker-compose.yaml -f docker-compose.libfaketime.yaml down

コンテナの停止・起動をPythonのスクリプトでコントロールする

手動でコンテナの上げ下げでもテストできるのですが、
自動テスト用にPythonでコンテナの制御もしてみます。

テストコードの実装

テストコードは次の通り。

ディレクトリ構成

  • docker-compose.yaml
  • docker-compose.libfaketime.yaml
  • app
    • main.py
    • requirements.txt
    • Dockerfile
  • libfaketime
    • Dockerfile
  • test.py ★追加
  • requirements.txt ★追加

各ファイルの内容

test.py

import time
import docker
import requests
import pytest

@pytest.fixture
def faketime_app(request):
    service, faketime = request.param
    client: docker.DockerClient = docker.from_env()

    # build arguments for new container
    container_ids = [
        c.id
        for c in client.containers.list(filters={'label': f"com.docker.compose.service={service}", 'status': 'running'})
    ]
    if len(container_ids) == 0:
        pytest.fail(f"{service} container is not running")
    container = client.containers.get(container_ids[0])
    environments = [
        e
        for e in container.attrs.get("Config", {}).get("Env")
        if e.split("=")[0] != "FAKETIME"
    ] + [
        f"FAKETIME={faketime}"
    ]
    create_args = {
        'image': container.attrs.get("Image"),
        'command': container.attrs.get("Config", {}).get("Cmd"),
        'ports': [pt.split('/')[0] for pt in container.attrs.get("HostConfig", {}).get("PortBindings", {})],
        'environment': environments,
        'volumes': container.attrs.get("Config", {}).get("Volumes"),
        'host_config': container.attrs.get("HostConfig"),
        'labels': container.attrs.get("Config", {}).get("Labels"),
    }

    # stop container
    client.api.stop(container.id)
    client.api.wait(container.id)

    # create and start faketime container
    new_container = client.api.create_container(**create_args)
    client.api.start(new_container.get("Id"))
    time.sleep(1)

    yield

    # stop faketime container
    client.api.stop(new_container.get("Id"))
    client.api.wait(new_container.get("Id"))
    client.api.remove_container(new_container.get("Id"))

    # start original container
    client.api.start(container.id)

@pytest.mark.parametrize(
    "faketime_app, expect_resp",
    [
        (("app", "2023-03-06 00:00:00"), "Working"),
        (("app", "2023-03-12 00:00:00"), "Holiday"),
    ],
    indirect=["faketime_app"]
)
def test_app(faketime_app, expect_resp):
    resp = requests.get('http://localhost:5000/', timeout=3)
    assert expect_resp in resp.text

requirements.txt

docker==6.0.1
pytest==7.2.2
requests==2.28.2

venv仮想環境のセットアップ

venv仮想環境を次のように準備する。

python -m venv venv
. venv/bin/activate
pip install -r requirements.txt

テスト実行

コンテナを起動する。

docker compose -f docker-compose.yaml -f docker-compose.libfaketime.yaml up -d --build

テストを実行する。

. venv/bin/activate
python -m pytest test.py

コンテナを停止する。

docker compose -f docker-compose.yaml -f docker-compose.libfaketime.yaml down

以上。