0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Django+MySQL+GitHub Actions] サービスコンテナを使ってdocker-composeを使わずにPytestを実行しよう

Last updated at Posted at 2023-01-26

前提

  • GitHub Actionsの基本的な用語についてある程度理解している
  • フレームワークはDjangoを使用
  • DBはMySQLを使用
  • Pytestを使用
  • Poetryを使用
  • PR内にカバレッジを表示させる方法についても説明

サービスコンテナとは

ワークフロー中でアプリケーションをテストもしくはビルドするのに必要なサービスを提供するためのDockerコンテナです
サービスコンテナを使うことでワークフロー内で例えばdocker-composeを使って自前でDBを作成せずにテストを実行することができます
runner内のリソースは限られているのでGitHub側で用意するサービスコンテナなどを使うケースが多いです
また、docker-composeを使わずにワークフローを実行するのでコンテナの起動時間分ワークフローの時間を短縮できます
今回はMySQLのサービスコンテナを使ってPytestを実行させます

必要なファイル一覧

以下のファイルを編集していきます

❯ tree 
.
├── .github
│   └── workflows
│       └── test.yml
└── application # Djangoのプロジェクトファイルおよびpyproject.tomlが入ったディレクトリ
    ├── project
    │   └── settings.py
    ├── manage.py
    ├── poetry.lock
    └── pyproject.toml

settings.py

settings.pyに以下のように設定します

settings.py
import os

SECRET_KEY = os.environ.get("SECRET_KEY")

DEBUG = os.environ.get("DEBUG") == "True"

ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS").split(" ")

# データベースの設定を行う
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": os.environ.get("MYSQL_DATABASE"),
        "USER": os.environ.get("MYSQL_USER"),
        "PASSWORD": os.environ.get("MYSQL_PASSWORD"),
        "HOST": os.environ.get("MYSQL_HOST", "db"),
        "PORT": os.environ.get("MYSQL_PORT", 3306),
    }
}

ワークフローの作成

.github/workflows/test.ymlにテストの自動実行までの処理を記載していきます

.github/workflows/test.yml
name: Run Pytest
on:
  pull_request:
    types: [opened, reopened, synchronize, ready_for_review]

env:
  WORKING_DIRECTORY: application
  SECRET_KEY: test
  DJANGO_SETTINGS_MODULE: project.settings
  ALLOWED_HOSTS: 127.0.0.1
  # "True"にしないとDEBUG内がFalseになってしまう
  DEBUG: "True"
  MYSQL_ROOT_PASSWORD: root
  MYSQL_DATABASE: test-db
  MYSQL_HOST: 127.0.0.1
  MYSQL_PORT: 3306
  MYSQL_USER: test
  MYSQL_PASSWORD: test

jobs:
  Test:
    if: |
      github.event.pull_request.draft == false
      && !startsWith(github.head_ref, 'release')
      && !startsWith(github.head_ref, 'doc')
    name: Run Test Code
    runs-on: ubuntu-20.04
    # ルート直下にpyproject.tomlを置いている場合は不要
    # 今回はapplication/内にPoetryを含めたDjangoのソースコードが含まれているため、指定する
    defaults:
      run:
        working-directory: ${{ env.WORKING_DIRECTORY }}
    # MySQLのサービスコンテナを設定
    services:
      db:
        # MySQLのDocker imageを使用
        image: mysql:8.0
        ports:
          - 3306:3306
        env:
          MYSQL_ROOT_PASSWORD: ${{ env.MYSQL_ROOT_PASSWORD }}
          MYSQL_DATABASE: ${{ env.MYSQL_DATABASE }}
          MYSQL_USER: ${{ env.MYSQL_USER }}
          MYSQL_PASSWORD: ${{ env.MYSQL_PASSWORD }}
        # MySQLより先にDjangoが起動しないようヘルスチェックを使って起動順を制御
        options: >-
          --health-cmd "mysqladmin ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - name: Chekcout code
        uses: actions/checkout@v3
      # MySQLのユーザに実行権限を付与
      - name: Grant privileges to user
        run: mysql --protocol=tcp -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD -e "GRANT ALL PRIVILEGES ON *.* TO '$MYSQL_USER'@'%'; FLUSH PRIVILEGES;"
      # Poetryのインストール
      - name: Install poetry
        run: pipx install poetry
      # Pythonのセットアップを実行
      # 2回目以降のテスト実行時はrunner内にPoetryのCacheを生成してPoetryのセットアップを高速化
      - name: Use cache dependencies
        uses: actions/setup-python@v4
        with:
          # pyproject.tomlからPythonのversionを指定(絶対パス)
          python-version-file: "${{ env.WORKING_DIRECTORY }}/pyproject.toml"
          cache: 'poetry'
      # Poetry内の必要なパッケージのインストールを実行
      - name: Install Packages
        run: poetry install
      - name: Run migration
        run: |
          poetry run python manage.py makemigrations
          poetry run python manage.py migrate
      # Pytestを実行
      - name: Run Pytest
        run: |
          set -o pipefail
          poetry runpytest --junitxml=pytest.xml -x -n auto --cov --no-cov-on-fail | tee pytest-coverage.txt
      # カバレッジをPRに表示
      - name: Pytest coverage comment
        uses: MishaKav/pytest-coverage-comment@main
        with:
          pytest-coverage-path: ${{ env.WORKING_DIRECTORY }}/pytest-coverage.txt
          junitxml-path: ${{ env.WORKING_DIRECTORY }}/pytest.xml

それでは、一つずつ解説していきます

ワークフローを実行する前の設定

以下のように冒頭で

  • どの場面でワークフローを実行するか
  • どの環境変数を使用するか

記載します
今回はがプルリクエスト

  • opened(作成時)
  • reopened(再作成時)
  • synchronize(pushするたびに)
  • ready_for_review(レビューできる状態になったら)

の時にワークフローを実行します
また、envを使うとワークフロー内で使用できる独自の環境変数を定義できます
今回はテストを実行するだけのワークフロー内に秘匿情報を使う必要がないです
そのため、下記のようにsecretsを使わずに環境変数を直書きしています

.github/workflows/test.yml
name: Run Pytest
on:
  pull_request:
    types: [opened, reopened, synchronize, ready_for_review]

env:
  WORKING_DIRECTORY: application
  SECRET_KEY: test
  DJANGO_SETTINGS_MODULE: project.settings
  ALLOWED_HOSTS: 127.0.0.1
  DEBUG: "True"
  MYSQL_ROOT_PASSWORD: root
  MYSQL_DATABASE: test-db
  MYSQL_HOST: 127.0.0.1
  MYSQL_PORT: 3306
  MYSQL_USER: test
  MYSQL_PASSWORD: test

テストの実行を制限したいとき

テストに関しては

  • draft
  • releaseブランチ
  • docブランチ(ドキュメント作成用)

の時は実行したくないので

.github/workflows/test.yml
if: |
      github.event.pull_request.draft == false
      && !startsWith(github.head_ref, 'release')
      && !startsWith(github.head_ref, 'doc')

を追記します

ワーキングディレクトリの指定

今回はapplication/内にPoetryを含めたDjangoのソースコードが含まれているため、以下のように記載します

.github/workflows/test.yml
    defaults:
      run:
        working-directory: {{ env.WORKING_DIRECTORY }}

MySQL用のサービスコンテナの設定

MySQLのサービスコンテナを設定する際に

  • Docker image
  • ポート
  • 環境変数
  • ヘルスチェック

を記載します
Docker imageは以下のMySQL8.0のものを使用します

MySQLの環境変数で必要なものは

  • MYSQL_ROOT_PASSWORD
  • MYSQL_DATABASE
  • MYSQL_USER
  • MYSQL_PASSWORD

です
上記がないとDBに接続できずにテストが失敗してしまうので必ず設定しましょう

optionsでヘルスチェックを行い、Djangoより先にMySQLの起動が完了するよう制御します

ヘルスチェックについて知りたい方は以下の記事を参照してください

.github/workflows/test.yml
    # MySQLのサービスコンテナを設定
    services:
      db:
        # MySQLのDocker imageを使用
        image: mysql:8.0
        ports:
          - 3306:3306
        env:
          MYSQL_ROOT_PASSWORD: ${{ env.MYSQL_ROOT_PASSWORD }}
          MYSQL_DATABASE: ${{ env.MYSQL_DATABASE }}
          MYSQL_USER: ${{ env.MYSQL_USER }}
          MYSQL_PASSWORD: ${{ env.MYSQL_PASSWORD }}
        # MySQLより先にDjangoが起動しないようヘルスチェックを使って起動順を制御
        options: >-
          --health-cmd "mysqladmin ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

MySQLのユーザの実行権限の付与

ワークフロー内で使うMySQLのユーザにアクセス権限を付与していきます
--protocol=tcpがないと127.0.0.1にアクセスする際はソケットファイル(mysql.sock)を見にいってしまって接続できないため、追加します
今回はテストの際に使用するMYSQL_USERに権限を付与します
また、$MYSQL_ROOT_PASSWORD$MYSQL_USERと指定するとワークフローで設定した環境変数を使用できます

.github/workflows/test.yml
      # MySQLのユーザに実行権限を付与
      - name: Grant privileges to user
        run: mysql --protocol=tcp -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD -e "GRANT ALL PRIVILEGES ON *.* TO '$MYSQL_USER'@'%'; FLUSH PRIVILEGES;"

どうして127.0.0.1を指定しているのか

docker-composeを使う際はsettings.pyのMYSQL_HOSTにservice名を指定することで名前解決でMySQLのIPアドレスを取得して接続していましたが、今回はDockerを使わずにランナー上で直接テストを実行しています
公式ドキュメントでも記載しているようにホストを127.0.0.1に指定します

ランナーマシン上でのジョブの実行
ランナーマシン上でジョブを直接実行する場合、localhost:port か 127.0.0.1:port を使ってサービスコンテナにアクセスできます。 GitHubは、サービスコンテナからDockerホストへの通信を可能にするよう、コンテナネットワークを設定します

Poetryの設定

公式ドキュメントに記載の通りPoetryの設定を行います

Poetryをインストールしたあとpyproject.toml内のパッケージをインストールしていきます

cache: 'poetry'

と指定することで2回目以降のpoetry installをCacheを使って高速化させます
スクリーンショット 2023-12-27 10.39.24.png

.github/workflows/test.yml
      # Poetryのインストール
      - name: Install poetry
        run: pipx install poetry
      # Pythonのセットアップを実行
      # 2回目以降のテスト実行時はrunner内にPoetryのCacheを生成
      # Poetryのセットアップを高速化
      - name: Use cache dependencies
        uses: actions/setup-python@v4
        with:
          # pyproject.tomlからPythonのversionを指定(絶対パス)
          python-version-file: "${{ env.WORKING_DIRECTORY }}/pyproject.toml"
          cache: 'poetry'
      # Poetry内の必要なパッケージのインストールを実行
      - name: Install Packages
        run: poetry install

マイグレーションの実行

DB内にテーブルを作成するためにマイグレーションを行います

.github/workflows/test.yml
      - name: Run migration
        run: |
          poetry run python manage.py makemigrations
          poetry run python manage.py migrate

Pytestの実行

Pytestを実行します
後述するカバレッジをPRに表示させるActionを使用する際にPytestが失敗したのにワークフローが成功してしまう不具合がありました

set -o pipefail

を記載することでPytestが失敗したら以下のコマンドが実行されず、ワークフローが異常終了してくれます

tee pytest-coverage.txt

以下のissueに記載されていました

.github/workflows/test.yml
      # Pytestを実行
      - name: Run Pytest
        run: |
          set -o pipefail
          # pytest.xmlとpytest-coverage.txtはapplication/の配下に作成される
          poetry run pytest --junitxml=pytest.xml -x -n auto --cov --no-cov-on-fail | tee pytest-coverage.txt

オプションは以下の通りです

オプション 説明
-x テストが一つでも失敗したらPytestを強制終了させる
-n auto マルチスレッドでPytestを実行。runnerは1コア2スレッドなのでauto=2
--no-cov-on-fail テストが失敗したらカバレッジを表示させない

-n auto

pytest-xdistをインストールすると上記のオプションを使用することができます

カバレッジをPRに表示

.github/workflows/test.yml
      # カバレッジをPRに表示
      - name: Pytest coverage comment
        uses: MishaKav/pytest-coverage-comment@main
        with:
          # パスは今回はカレントディレクトリではなく、applicationを指定
          pytest-coverage-path: ${{ env.WORKING_DIRECTORY }}/pytest-coverage.txt
          junitxml-path: ${{ env.WORKING_DIRECTORY }}/pytest.xml

以下のActionを使用します

ワークフローを実行しよう

PRを作成したらワークフローが実行されます
下記のようにテストが実行できたら成功です
スクリーンショット 2023-07-05 8.14.58.png
スクリーンショット 2023-07-05 8.15.37.png

また、以下のようにPRにカバレッジが表示させることも確認できました

スクリーンショット 2023-07-05 8.15.57.png
スクリーンショット 2023-07-05 8.16.11.png

参考

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?