tag:crieit.net,2005:https://crieit.net/tags/alembic/feed 「alembic」の記事 - Crieit Crieitでタグ「alembic」に投稿された最近の記事 2021-06-26T20:15:08+09:00 https://crieit.net/tags/alembic/feed tag:crieit.net,2005:PublicArticle/17446 2021-06-26T20:15:08+09:00 2021-06-26T20:15:08+09:00 https://crieit.net/posts/pytest-sqlalchemy-alembic 📝 pytest で alembic のマイグレーションを行う方法 <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://fastapi.tiangolo.com/ja/">FastAPI</a> と <a target="_blank" rel="nofollow noopener" href="https://github.com/sqlalchemy/sqlalchemy">SQLAlchemy</a> を利用して Web API 開発を行っていた際、SQLAlchemy のマイグレーションツールである <a target="_blank" rel="nofollow noopener" href="https://github.com/sqlalchemy/alembic">alembic</a> を利用していました。</p> <p>ただ E2E テストを書こうとした際に、pytest 実行中に alembic でデータベースマイグレーションを行う方法が分からず模索していました。結果的にマイグレーションのやり方は分かったものの一応今後も利用するかもしれないため、その内容を記事として残しておくことにしました。</p> <p><strong>本記事内で利用しているソースコードを含む FastAPI プロジェクトを <a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/fastapi-sqlalchemy-alembic-pytest-sample">GitHub リポジトリ</a>上にアップしておいたので、詳細を確認されたい方がいればご参照くださいませ。</strong></p> <h1 id="alembic でマイグレーションを行う"><a href="#alembic+%E3%81%A7%E3%83%9E%E3%82%A4%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E8%A1%8C%E3%81%86">alembic でマイグレーションを行う</a></h1> <p><code>conftest.py</code> にグローバルで利用するマイグレーション用の <code>fixture</code> を定義すれば OK です。</p> <pre><code class="python"># conftest.py import os import alembic.config import pytest from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from sqlalchemy_utils import database_exists, create_database, drop_database # テスト用の初期データを定義した module を import する (必要があれば) # from .seed import users, contents # 指定したパラメータを用いて alembic によるデータベースマイグレーションを行う # 引数のデフォルト設定では全てのマイグレーションを実行するようになっている def migrate(migrations_path, alembic_ini_path='alembic.ini', connection=None, revision="head"): config = alembic.config.Config(alembic_ini_path) config.set_main_option('script_location', migrations_path) if connection is not None: config.attributes['connection'] = connection alembic.command.upgrade(config, revision) # テスト実行用にセットアップされたデータベースのセッション情報を扱う関数 # scope に session を指定することでテスト全体で一回だけ実行されるようにする @pytest.fixture(scope="session", autouse=True) def SessionLocal(): test_sqlalchemy_database_url = os.environ['DATABASE_URL'] engine = create_engine(test_sqlalchemy_database_url) # 既にテスト用データベースが存在していたら破棄する if database_exists(test_sqlalchemy_database_url): drop_database(test_sqlalchemy_database_url) # テスト用データベースを作成する create_database(test_sqlalchemy_database_url) # 環境変数 DATABASE_URL で指定したデータベースに対して、 # マイグレーションを行いテスト実行に必要なテーブルを一括作成する # 第一引数に指定している alembic は `alembic init <環境名>` 実行時に指定した環境名を入力 with engine.begin() as connection: migrate("alembic", 'alembic.ini', connection) Base = declarative_base() Base.metadata.create_all(engine) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # テスト用の各種データを追加する (必要があれば) # db_session = SessionLocal() # for user in users: # db_session.add(user) # db_session.commit() # for content in contents: # db_session.add(content) # db_session.commit() # db_session.close() # テスト用データ追加後のセットアップ済みの状態で # テスト用に利用する SessionLocal を返却する yield SessionLocal # テストが全て終わったら、テスト用データベースを破棄して、 # SQLAlchemy のセッションも切断する drop_database(test_sqlalchemy_database_url) engine.dispose() </code></pre> <h1 id="FastAPI の pytest への適用例"><a href="#FastAPI+%E3%81%AE+pytest+%E3%81%B8%E3%81%AE%E9%81%A9%E7%94%A8%E4%BE%8B">FastAPI の pytest への適用例</a></h1> <p>上記を関数を利用する方法は各自のテスト環境によって異なると思いますが、一応私が FastAPI のテストコードを書く際に利用したソースコードを元に参考例を載せておきます。</p> <p><code>conftest.py</code> と同じディレクトリに <code>client.py</code> を作成します。</p> <pre><code class="python"># client.py from fastapi import Header, HTTPException, status from fastapi.testclient import TestClient from app.dependencies import get_database from app.main import app # conftest で定義した fixture の SessionLocal を元に、 # データベースセッションを作成するための override_get_db 関数を定義して、 # get_database の代わりに override_get_db を実行するよう差し替える def temp_db(f): def func(SessionLocal, *args, **kwargs): def override_get_db(): db = SessionLocal() try: yield db finally: db.close() app.dependency_overrides[get_database] = override_get_db f(*args, **kwargs) app.dependency_overrides[get_database] = get_database return func client = TestClient(app) </code></pre> <p>あとは <code>pytest</code> のコード内で下記のような記述を行えば、FastAPI の内部でテスト用データベースを利用してくれるようになります。</p> <pre><code class="python"># test_main.py from fastapi import status from .client import client, temp_db # temp_db fixture を定義しておくことで、 # 関数の実行中は FastAPI の内部でテスト用データベースを利用する @temp_db def test_read_me_token_valid(): response = client.get("/users/me", headers={"Authorization": "Bearer 1234567890"}) assert response.status_code == status.HTTP_200_OK #... </code></pre> <h2 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/bee2/items/ff9c86d8d345dbcab497#2-%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9%E6%AF%8E%E3%81%ABdb%E3%82%92%E4%BD%9C%E6%88%90%E3%81%97%E4%BB%96%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E3%82%B1%E3%83%BC%E3%82%B9%E3%82%84%E6%9C%AC%E7%95%AA%E7%94%A8%E3%81%AEdb%E3%81%AB%E5%BD%B1%E9%9F%BF%E3%82%92%E4%B8%8E%E3%81%88%E3%81%9A%E3%81%ABapi%E3%81%AEunittest%E3%82%92%E8%A1%8C%E3%81%86">FastAPI でテスト用のクリーンな DB を作成して pytest で API の Unittest を行う - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/_akiyama_/items/9ead227227d669b0564e">pytest:フィクスチャ(fixture)の使い方 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://fastapi.tiangolo.com/tutorial/dependencies/">Dependencies - First Steps - FastAPI</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.pytest.org/en/6.2.x/">pytest: helps you write better programs — pytest documentation</a></li> </ul> nikaera