a_hamada/ 2025年 8月 29日/ 技術

はじめに

こんにちは!濱田です!
先日Djangoアプリケーションのplaywrightを使用したE2Eテスト環境をdocker-composeを使用して構築する機会がありました。
DBをdjango-tenantsでスキーマをきっていたり、テストの実行ごとにスキーマのデータをクリアしたりなど少し珍しいことをしていると思いましたので
何をやったか書いていこうと思います。

前提環境

今回の環境は以下の構成で構築しました。
・コンテナ1(Django+uwsgi用コンテナ)→E2Eテストもここで動きます。
 ベースイメージ:postgres:17.2-bullseye
・コンテナ2(Redis用コンテナ)
 ベースイメージ:redis:8.0.3
・コンテナ3(postgresDB用コンテナ)

Docker定義

次に今回使用したdocker-compose.yamlを以下にご紹介します。
※特筆する事項はその行の末尾に追記しています。

services:
  # uwsgi + djangoをホストするサービス
  django:
    build:
      context: .
      dockerfile: ./E2Etest/Dockerfile
    env_file:
      - .env_E2E
    environment:
      - DJANGO_ENV_FILE=.env_E2E    →☆Djangoで参照する環境変数をE2Eテスト用のものに差し替え
    container_name: django-container
    command: >       →☆コンテナ起動時の実行処理を記述
      sh -c "
        bash E2Etest/django_cmd.sh &&   →☆初期実行するDjangoのコマンドを記述したbashを実行
        sleep 5 &&
        poetry run pytest -srfE  E2Etest/   →☆E2Eテストをpytestで実行
      "
    ports:
      - '8000:8000'
    depends_on:
      - test-db
    volumes:
      - ./E2Etest/output:/usr/src/app/E2Etest/output
      - ./E2Etest/logs:/usr/src/app/E2Etest/logs
      - ./log:/usr/src/app/log
  # Redisサービス
  redis:
    image: redis:8.0.3      →☆ベースイメージは公式イメージ
    ports:
      - '26379:6379'
    hostname: redis-container
    container_name: redis-container

  # DBをホストするサービス
  postgres-db:
    image: postgres:17.2-bullseye   →☆ベースイメージは公式イメージ
    volumes:
      - ./test_db/db:/var/lib/postgresql/data   →☆DBのデータをボリュームマウント ※ここ重要
    ports:
      - '25432:5432'
    hostname: test-db
    environment:
      - POSTGRES_USER=test-test
      - POSTGRES_PASSWORD=test
      - POSTGRES_DB=test-test
      - TZ=Asia/Tokyo

次にDjangoのコンテナのDockerfileの定義です。

FROM ubuntu:24.04

RUN apt-get update && \
    apt-get install -y software-properties-common
RUN add-apt-repository ppa:deadsnakes/ppa
RUN apt-get update
RUN apt-get install -y python3.11 \
    python3.11-dev \
    python3-pip \
    libffi-dev \
    build-essential \
    fonts-noto-cjk \
    libreoffice \
    curl \
    curl libnss3 libatk-bridge2.0-0 libx11-xcb1 libxcb-dri3-0 libgbm1 libxcomposite1 \
    && ln -s /usr/bin/python3.11 /usr/bin/python \
    && rm -rf /var/lib/apt/lists/*      →☆必要パッケージをaptーgetでインストール

ENV PATH="/root/.local/bin:$PATH"

RUN curl -sSL https://install.python-poetry.org | python3 \
    && find / -name poetry

RUN python3.11 -m pip install playwright tzdata     →☆playwrightのインストール

COPY ./ /usr/src/app/       →☆ソースコードを一式コピー

WORKDIR /usr/src/app/       →☆ワークディレクトリの変更

# RUN poetry install
RUN /root/.local/bin/poetry install --no-root --with e2e        →☆poetry環境に開発用パッケージのインストール
RUN /root/.local/bin/poetry run playwright install --with-deps chromium     →☆poetry環境にplaywrightで必要なオプションのインストール

次にdocker-compose.yamlで実行したDjangoの初期コマンドのスクリプトがこちらです。

poetry run python manage.py migrate_schemas --shared
poetry run python manage.py collectstatic --noinput 
poetry run python manage.py create_tenant --schema=demo --tenant_code=demo --domain-domain=127.0.0.1 --domain-is_primary=True --noinput     →☆django-tenantsを使用してdemoという名前でスキーマを作成
poetry run python manage.py insert_data -m insert_system_data
poetry run python manage.py tenant_command insert_data -m insert_tenant_data -t demo --schema=demo      →☆django-tenantsを使用してスキーマにデータを初期投入
poetry run python manage.py runserver 0.0.0.0:8000 &

E2Eテストの各種設定

・通常のUT実行と環境を分ける
下記のようにE2Eテストの実行時と通常のUT実行時で違う設定を使用するために環境変数で判断できるようにします。
ENV_NAME=E2E

次にE2Eテストを構築するにあたって使用した各種設定を紹介します。
・pytestでも通常のアプリ起動時と同じDBを使用する
pytest用のsettings.pyに下記の記述を追加します。
通常のUTの環境と競合しないようにE2Eの場合のみデータベースの設定を上書きします。

if ENV_NAME == "E2E":
    DATABASES["default"]["TEST"] = {"NAME": DATABASES["default"]["NAME"]}

・Djangoのデバッグツールバーを非表示にする
E2Eテスト実行時Djangoのデバックツールバーが表示されていると、クリックなどの操作を阻害してしまう可能性があるので
非表示にするように設定する
環境変数とsettings.pyにそれぞれ以下の項目を追加する。
環境変数↓

DISABLE_DEBUG_TOOLBAR=True

settings.py側の設定

def show_toolbar(request):
    if os.environ.get("DISABLE_DEBUG_TOOLBAR", False):
        return False
    return True

テスト実行のたびにスキーマのデータをリセットする

次に今回のE2Eテストではテスト実行ごとにDBのデータをクリアした方が勝手が良さそうだったので、
DBクリア用のカスタムコマンドを実装しました。
実装したコマンドは以下の通りです。

import subprocess
from django.core.management.base import BaseCommand, CommandError
from django.db import connection
from django_tenants.utils import get_tenant_model, schema_exists
from logging import getLogger

logger = getLogger(__name__)

class Command(BaseCommand):
    help = "Clear all model data in the specified app, respecting FK constraints"

    def add_arguments(self, parser):
        parser.add_argument("-s", "--schema_name", type=str, help="削除するスキーマ名")    →☆コマンドの引数で削除対象のスキーマ名を取得

    def handle(self, *args, **options):
        logger.info(connection.settings_dict["HOST"])
        logger.info(connection.settings_dict["NAME"])
        logger.info("start restore_schema")
        schema_name = options["schema_name"]

        TenantModel = get_tenant_model()    →☆django-tenantsの標準関数でテナントモデルを取得

        logger.info("get tenant model")
        try:
            tenant = TenantModel.objects.get(schema_name=schema_name)   →☆受け取ったスキーマ名のオブジェクトを取得
        except TenantModel.DoesNotExist:
            raise CommandError(f"スキーマ名 '{schema_name}' に対応するテナントが見つかりません。")

        if schema_name == "public":       →☆念のためpublicスキーマの削除はブロック
            raise CommandError("public スキーマは削除できません。")

        if not schema_exists(schema_name):
            self.stdout.write(self.style.WARNING(f"スキーマ '{schema_name}' はすでに存在しません。"))
            return

        self.stdout.write(f"スキーマ '{schema_name}' を削除中...")

        with connection.cursor() as cursor:
            cursor.execute(f'DROP SCHEMA "{schema_name}" CASCADE;')     →☆スキーマ削除コマンドを実行

        self.stdout.write(self.style.SUCCESS(f"スキーマ '{schema_name}' を削除しました。"))
        logger.info("create schema")
        tenant.create_schema(check_if_exists=True)      →☆同じスキーマを再作成
        logger.info("insert data")
        connection.close()
        result = subprocess.run(
            [
                "python",
                "manage.py",
                "tenant_command",
                "insert_data",
                "--mode=insert_tenant_data",
                "--tenantcode=demo",
                "--schema=demo",
            ],
            capture_output=True,
            text=True,
        )       →☆スキーマに初期データを投入

        logger.info("end restore_schema")

次にpytestからも実行できるようにconftest.pyにpytest.fixtureとして定義します。

from django.core import management

@pytest.fixture(autouse=True, scope="function")       →☆scope="function"として関数ごとにDBスキーマがリセットされるように設定
def clear_schema_data(db):
    management.call_command("restore_schema", schema_name="demo")       →☆Djangoのmanagementを使用してDjangoのカスタムコマンドを実行します。

まとめ

今回はdocker-composeを使用してplaywrightのE2Eテスト環境を作成したときの手順をご紹介しました。
django-tenantsを使用してスキーマを切っていたり、テスト実行ごとにスキーマのデータをリセットしていたり
E2Eテストを構築する上で使えそうなノウハウがあるかなと思い、ご紹介しました。
もし困っている方やこれから環境を構築しようと思っている方の一助になれば幸いです。

それではまた!

About a_hamada

2020年9月からWebプログラマに転向した半人前 日々勉強することばかり。 最近使っている言語はもっぱらPython