はじめに
開発を進める上で、コードの品質を保つために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
で管理しています。
[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に影響を与えないという設計で実装します。
テスト環境のイメージ
今回実装するテスト環境のイメージを図で表すと以下のようになります。
テスト実行時にはテストDB用のSessionを作成し、アプリで作成しているSessionをオーバライドすることでアプリDBではなく、テストDBに実行処理が反映されるようにしました。
それぞれ以下の順序で実装を行いました。
- テスト用にPostgreSQLコンテナを作成
- pytestのインストール
- pytest用のSessionの作成
- テストファイルでSessionをオーバライド
それぞれ順を追って説明できればと思います。
テスト用にPostgreSQLコンテナを作成
docker-compose.yml
で以下のように記載してDBコンテナを作成しています。
アプリ用DBの設定とほとんど同じで、テスト用DBとして区別するために少しだけ修正しています。
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コンテナに接続する情報を記載しています。
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件保存処理をするテストを記載しています。
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]
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ツールとしてautopep8
や yapf
、Pylint
、isort
などがあります。公式ドキュメントもありますので、ご参照ください。
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ファイルは以下の内容になります。
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.py
をconftest.py
に上書きをしてpytest実行
これらを踏まえて、ワークフローを定義したyamlファイルは以下の通りになります。
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
に変更しています。
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チェックも通ります。
最後に
他にもいい設定はあるはずですが、今回FastAPIのCI構築の一例として記事に残しました。
FastAPIのCI構築をしたい方のヒントになれば幸いです。
ここまで読んでいただいてありがとうございました🙇♂️
参考文献