LoginSignup
12
8

FastAPIのCI環境を構築してみた(FastAPI・pytest・flake8・Black・GithubActions)

Last updated at Posted at 2023-12-04

はじめに

開発を進める上で、コードの品質を保つためにTest環境の構築やlint・formatterを設定する場面があると思います。
今回はFastAPIのアプリに、pytestを用いたテスト環境とflake8のlintチェックやBlackのformatterを実装してみたので、記事として残したいと思います。
GithubActionsでCIを構築するところまで解説したいと思いますので、同じような実装をしたい方の参考になれば嬉しいです。

作りたいもののゴール

今回は以下のとおりCI環境を構築したいと思っています。

  • test・lint・formatterの実行環境を構築
  • GithubActionsにtestとlintのCIを設定する。

ソースコード

以下のソースコードのリポジトリを貼らせていただきます。
主にコードの解説になりますので、こちら適宜参考にしていただければと思います。

使用する技術・ライブラリについて

以下の技術やライブラリを選定しています。
今回はCI構築がメインなので、ライブラリごとの比較はそこまで重要視せず選定を行いました。

  • Python: 3.11
  • FastAPI: 0.104.1
  • Poetry(パッケージ管理ツール)
  • pytest(テストライブラリ)
  • flake8(静的解析ツール)
  • black(formatterツール)

全体としては以下pyproject.tomlで管理しています。

pyproject.toml
[tool.poetry]
name = "app"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.104.1"
uvicorn = {extras = ["standard"], version = "^0.24.0.post1"}
sqlalchemy = "^2.0.23"
alembic = "^1.12.1"
psycopg2 = "^2.9.9"
pydantic = "^2.5.0"
sqlalchemy-utils = "^0.41.1"
httpx = "^0.25.1"


[tool.poetry.group.dev.dependencies]
pytest = "^7.4.3"
flake8 = "^6.1.0"
black = "^23.11.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

テスト環境の構築

テストツールにはpytestを使用しています。
pytestとは、Python用に設計された単体テスト用のフレームワーク(テスティングフレームワーク)です。以下公式ドキュメントを添付しますので、ご参照ください。

今回Dockerを使用してFastAPIの環境構築をしており、DBもpostgresイメージのコンテナを作成しています。
そのため、テスト用DBコンテナを使用してテストを実行できる環境を構築しようと思います。具体的には、テスト実行時のDBセッションの接続先をテストDBに切り替え、アプリDBに影響を与えないという設計で実装します。

テスト環境のイメージ

今回実装するテスト環境のイメージを図で表すと以下のようになります。

Image from Gyazo

テスト実行時にはテストDB用のSessionを作成し、アプリで作成しているSessionをオーバライドすることでアプリDBではなく、テストDBに実行処理が反映されるようにしました。
それぞれ以下の順序で実装を行いました。

  1. テスト用にPostgreSQLコンテナを作成
  2. pytestのインストール
  3. pytest用のSessionの作成
  4. テストファイルでSessionをオーバライド

それぞれ順を追って説明できればと思います。

テスト用にPostgreSQLコンテナを作成

docker-compose.ymlで以下のように記載してDBコンテナを作成しています。
アプリ用DBの設定とほとんど同じで、テスト用DBとして区別するために少しだけ修正しています。

docker-compose.yml
version: '3'

services:

  ...中略...
  
  db-test:
    image: postgres:15
    container_name: postgres-db-test
    volumes:
      - postgres_data_test:/var/lib/postgresql/data/
    environment:
      - POSTGRES_USER=postgres-test
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=postgres-test
    ports:
      - 5433:5432

volumes:
  postgres_data:
  postgres_data_test:

アプリ用DBのポート番号とバッティングしないように5433と指定してます。
以下のコマンドで、Dockerイメージをビルドしてコンテナ起動しておきます。

ターミナル
docker-compose up -d --build

pytestのインストール

Dockerコンテナを使用しているため、docker-compose exec appを接頭に置き、pytestをインストールするコマンドを実行します。

ターミナル
docker-compose exec app poetry add pytest --dev

pytest用のSessionの作成

conftest.pyにデータベース用のSessionを定義します。
以下コードの実装例で、テストDBコンテナに接続する情報を記載しています。

tests/conftest.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.db import Base


def override_session_factory():
    # ①テストDBコンテナの情報をセット
    TEST_DATABASE_URL = (
        "postgresql://postgres-test:password@postgres-db-test:5432/postgres-test"
    )

    # ②①の情報を元にデータベースのEngineを作成
    engine = create_engine(
        url=TEST_DATABASE_URL, echo=False, pool_recycle=10, isolation_level="AUTOCOMMIT"
    )

    # ③sessionmakerでSessionオブジェクトを作成する
    SessionLocal = sessionmaker(
        autocommit=False, autoflush=True, expire_on_commit=False, bind=engine
    )

    # ④一括してテーブルを作成
    Base.metadata.create_all(engine)

    # ⑤SessionLocalのインスタンスを作成し、呼び出し元に適切に渡す操作を行う。
    session = SessionLocal()
    try:
        yield session
    finally:
        session.close()

①では、作成したテストDBのコンテナに合わせてDATABASEのURLをセットします。
その上で、②においてデータベースのEngineを作成、sessionmakerメソッドでSessionオブジェクトを作成しています。
④のBase.metadata.create_all(engine)では、アプリ内で使用するテーブルを一括して作成します。
最後に⑤で、テストで使用するSessionオブジェクトのインスタンスsessionを定義しyieldで渡す処理を実装しています。

【テストDBコンテナのportについて(補足)】
docker-compose.ymlでは、テストDBのポートを5433:5432としていますが、TEST_DATABASE_URLでは5432としています。
当初TEST_DATABASE_URLのポートを5433に指定するのだと思ったのですが、以下のエラーが発生して接続できないという事象が発生しました。

ターミナル
E     sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) could not connect to server: Connection refused
E     Is the server running on host "postgres-db-test" (172.19.0.2) and accepting
E     TCP/IP connections on port 5433?
E
E     (Background on this error at: https://sqlalche.me/e/20/e3q8)

.venv/lib/python3.11/site-packages/fastapi/concurrency.py:36: OperationalError

こちら、ホスト(ローカル端末)では5433にマッピングされていますが、Dockerのネットワーク内部では5432にマッピングされており、今回みたいなアプリコンテナ(fastapiのコンテナ)からテストDBコンテナにアクセスする場合のポート番号は、5432となるみたいです。
以下参考にした記事を添付させていただきますので、ご参照ください🙇‍♂️

テストファイルでSessionをオーバーライド

色々設定があるかと思いますが、今回はテストファイルごとにアプリのSessionをoverride_session_factory()にオーバーライドするように実装してます。
そうすることで、テスト実行時はアプリDB用のSessionではなく、テストDB用のSessionを使用することが可能になります。
以下、サンプルのテスト例として1件保存処理をするテストを記載しています。

tests/test_item.py
from starlette.testclient import TestClient
from src.main import app
from fastapi import status
from tests.conftest import override_session_factory
from src.db import session_factory

client = TestClient(app)

# アプリ自体のSessionを`override_session_factory`にオーバーライド
app.dependency_overrides[session_factory] = override_session_factory


def test_create_item():
    response = client.post("/items", json={"name": "foo", "description": "bar"})

    assert response.status_code == status.HTTP_200_OK

上記の設定が終わったら、以下コマンドを打つことでテストが走ります。
コンテナ起動中であることを確認してください。

ターミナル
docker-compose exec app poetry run pytest

以下実行され、ターミナルにはテスト結果が表示されます。

ターミナル
docker-compose exec app poetry run pytest
================ test session starts ===================================================
platform linux -- Python 3.11.4, pytest-7.4.3, pluggy-1.3.0
rootdir: /app
plugins: anyio-3.7.1
collected 1 item

tests/test_item.py .                                                              [100%]

================ 1 passed in 0.63s =====================================================

【補足】
テスト実行後にDBをDROPする処理など、追加で実装できる部分は色々ありますが、今回はそこまで設定ができませんでした。
以下の記事で詳しく記載されていますので、ご参照いただければと思いますm(_ _)m

lintの導入

pythonのlintチェックのライブラリは複数ありますが、今回はflake8を導入してみました。
インストールして.flake8という設定ファイルを作成すれば問題なくセッティングはできます。

flake8について

Pythonの静的解析を行うツールの1つで、Pythonコードの品質を向上させるために使用します。
flake8は以下3つのlintツールをまとめたラッパーです。

  • pycodestyle
  • pyflakes
  • mccabe

PEP8というPythonのコーディングスタイルに関する公式のガイドラインに準拠してコードを精査してくれます。公式ガイドもありますので、ご参照ください。

flake8のインストール

以下コマンドによりflake8をインストールします。

ターミナル
docker-compose exec app poetry add flake8 --dev

.flake8の設定

.flake8を設定することで、flake8の動作をカスタマイズできます。
例えば、特定のルールを無視する指示(ignore)や、チェックをスキップするディレクトリを設定(exclude)することなどが可能です。
以下、今回のリポジトリで設定している.flake8を掲載いたします。

.flake8
[flake8]
max-line-length = 88
ignore = E203
exclude = .git, .github, .venv/, .dockerenv/, 

実行して修正箇所の指摘がある場合は、以下のように表示されます。
今回は使用していない不要なimportを探知して指摘してくれています。

ターミナル
docker-compose exec app poetry run flake8
./src/routers/item.py:7:1: F401 'logging' imported but unused
make: *** [lint] Error 1

formatterの導入

formatterについても種類は色々あるが、今回はBlackを使用します。
カスタマイズ性は低く、インストールすれば細かい設定は不要で、すぐformatterの実行が可能です。

Blackについて

コード整形のライブラリの一つです。Black以外のFormatterツールとしてautopep8yapfPylintisortなどがあります。公式ドキュメントもありますので、ご参照ください。

Blackのインストール

コンテナを起動している状態で以下のコマンドを打つことで、Blackがコンテナ実行環境にインストールされます。

ターミナル
docker-compose exec app poetry add black --dev

実行方法

こちらについては導入後以下のように打つことで、formatterが起動しファイルの整形をしてくれます。
今回はsrcディレクトリとtestsディレクトリにあるpythonコードを整形しようと思います。
以下コンテナ起動中に実行すれば、formatterが起動します。

ターミナル
docker-compose exec app poetry run black src tests

以下実行後のターミナルです。
整形されたファイルのパスが列挙され、整形完了したことが分かります。

ターミナル
docker-compose exec app poetry run black src tests
reformatted /app/tests/test_item.py

All done! ✨ 🍰 ✨
1 file reformatted, 14 files left unchanged.

GithubActionsの設定

これで、ローカルにおけるテスト環境の構築とLintチェック、Formatterの設定が完了しました。
最後にGithubActionsを用いてpushするたびにlintとtestのチェックが入るCIを構築します。
以下GithubActionsのドキュメントです。

ワークフローの設定はシンプルでリポジトリに以下のようなファイルを設定

lintに関する設定

以下のようなyamlファイルをworkflowsディレクトリ(.github/workflows/)に作成し、フローを記載します。
yamlファイルは.github/workflows/に保存しないと機能しませんので、ご注意ください。
Lintチェックの設定のためにYamlファイルでは以下の流れで作成しています。

  • Poetryをインストール
  • Python3.11の環境をセットアップ
  • poetry install --no-rootを実行し、依存するライブラリをインストール
  • flake8の実行

最終的に作成したyamlファイルは以下の内容になります。

.github/workflows/lint.yml
name: Flake8 Lint

on: [push]

jobs:
  flake8-lint:
    runs-on: ubuntu-latest
    name: Lint

    steps:
      - name: Check out source repository
        uses: actions/checkout@v4

      - name: Install Poetry
        run: pipx install poetry 

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.11"
          cache: "poetry"

      - name: install dependencies
        run: poetry install --no-root

      - name: Lint with flake8
        run: poetry run flake8 src tests

Testに関する設定

pytestの実行も同様に.github/workflows/test.ymlというファイルを作成してフローを定義しています。
ローカル環境でも、テスト実行時のDBは別途コンテナを立てているので、今回定義するワークフローでもPostgreSQLのコンテナを作成して実行環境を用意しています。

上記を踏まえてテストのワークフローは、以下の流れで作成しています。

  • PosgreSQLのサービスコンテナを立ち上げる
  • Poetryをインストール
  • Python3.11の環境をセットアップ
  • poetry install --no-rootを実行し、依存ライブラリをインストール
  • テスト用のconftest_ci.pyconftest.pyに上書きをしてpytest実行

これらを踏まえて、ワークフローを定義したyamlファイルは以下の通りになります。

.github/workflows/test.yml
name: Pytest

on: [push]

env:
  POSTGRES_USER: postgres-test
  POSTGRES_PASSWORD: password
  POSTGRES_DB: postgres-test

jobs:
  test:
    runs-on: ubuntu-latest
    name: Test

    services:
      db-test:
        image: postgres:15.4
        env:
          POSTGRES_USER: ${{ env.POSTGRES_USER }}
          POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
          POSTGRES_DB: ${{ env.POSTGRES_DB }}
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Check out source repository
        uses: actions/checkout@v4

      - name: Install poetry
        run: pipx install poetry

      - name: Use cache dependencies
        uses: actions/setup-python@v4
        with:
          python-version: "3.11"
          cache: 'poetry'

      - name: install dependencies
        run: poetry install --no-root

      - name: pytest run
        run: | 
          cp tests/conftest_ci.py tests/conftest.py
          poetry run pytest

最後の手順(cp tests/conftest_ci.py tests/conftest.py)ですが、元々作成したconftest.pyでは、GithubActions上でDBコンテナにアクセスできないエラーが発生しました。
調べたところ、先ほど添付したドキュメントで以下の文言があり、GithubActions上のサービスコンテナのホストがlocalhostとなっていたことが原因でした。

ランナーマシン上でジョブを直接実行する場合、localhost:<port>127.0.0.1:<port>を使ってサービスコンテナにアクセスできます。

つまり現状のconftest.pyで設定しているTEST_DATABASE_URLではホスト名が異なっていたため、GithubActionsではDBに接続できないという状況になっていました。
他に良い設定方法はあるはずなんですが、今回以下のようにTEST_DATABASE_URLのみ変更したconftest_ci.pyというファイルを別途作り、GithubActions上でのみconftest.pyに上書きする処理でエラーから逃れました。
以下conftest_ci.pyのファイルですが、DATABASE_URLのホスト名のみlocalhostに変更しています。

tests/conftest_ci.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from src.db import Base


def override_session_factory():
    # host名のみlocalhostに変更 
    TEST_DATABASE_URL = (
        "postgresql://postgres-test:password@localhost:5432/postgres-test"
    )
    engine = create_engine(
        url=TEST_DATABASE_URL, echo=False, pool_recycle=10, isolation_level="AUTOCOMMIT"
    )
    SessionLocal = sessionmaker(
        autocommit=False, autoflush=True, expire_on_commit=False, bind=engine
    )

    Base.metadata.create_all(engine)

    session = SessionLocal()
    try:
        yield session
    finally:
        session.close()

これらの設定を行った上で、githubにpushするとCIが走ります。
LintやTestに違反している場合はCIが通らず、違反していなければCIチェックも通ります。

Image from Gyazo

最後に

他にもいい設定はあるはずですが、今回FastAPIのCI構築の一例として記事に残しました。
FastAPIのCI構築をしたい方のヒントになれば幸いです。
ここまで読んでいただいてありがとうございました🙇‍♂️

参考文献

12
8
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
12
8