LoginSignup
0
3

More than 3 years have passed since last update.

データベースをDockerでfixtureするpytest環境例

Last updated at Posted at 2020-11-07

目的

データベースを利用したPythonアプリのユニットテストをpytestで楽に環境の影響を受けず実行したい。(SQLite3等のファイルではなくデーモンとして立ち上がるもの)

できたもの

サンプルコード: https://github.com/hkato/pytest-docker-mysql-example

pytestからMySQLをDocker上で立ち上げスキーマとテストデータを投入しテスト実行する。docker-composeの設定とdocker-entrypoint-initdb.dのSQL関連の対応をすればPostgreSQLなど他のデータベースでも基本同じはず。(docker-entrypoint-initdb.dでの初期化機構はMySQL/PostgreSQLのオフィシャルコンテナーは同様の動作のはずだが他は知らない)

必要な環境やpytest plugin

  • Python3
    • pytest
    • pytest-docker
    • pytest-dotenv
  • Docker

pytest関係で、Dockerを立ち上げたり、データベースを扱うプラグインを見てみたが、pytest-dockerがメンテ状況や自分の用途・環境には良さそう。pytest-dotenvはDockerとpytestで環境変数を.envファイルに記述し共通化するために使用した。

その他の検討

pytest-docker以外に多少検討してみたものとして、

  • pytest-docker-compose: 少し古い。pytest-dockerはdocker-composeを扱うのでメンテナンスやドキュメントがしっかりしたpytest-dockerの方が良いという印象。
  • pytest-dbfixtures シリーズ: 主流の各データーベースに対応
    • pytest-mysql: ちゃんと見てないが、基本クライアントとして振る舞うので、Dockerの扱いが面倒そう。

結局、pytest-dockerはデータベースによらず、公式ドキュメントにあるようにWebアプリのコンテナーを使ったり、汎用的に利用できるので一番良さそう(MinIOのコンテナーを起動しAWS S3代わりにもしようと思っている)。

ディレクトリ構成

.
├── src                 # アプリケーションコード
│   └── *.py
├── tests               # テストコード
│   ├── conftest.py
│   └── test_*.py
├── initdb.d            # データーベーススキーマ&テストデータなど
│   ├── *.sql
│   └── *.sh
├── pytest.ini          # pytest設定
├── .env                # 環境変数設定
└── docker-compose.yml  # Docker設定

コード概要

conftest.py

docker-compose.ymlの配置対応

pytest-dockerはデフォルトで tests/ の下の docker-compose.yml を参照する。プロジェクト直下に設定することにより(別ディレクトリやファイル名の指定はpytest-dockerのREADME.mdにサンプルにあり)、Webアプリなどの場合は docker-compose up -d してデータベースを動かしつつ、Gunicorn, uWSGI, UvicornなどでWebアプリサーバーを実行してデバッグできる(今回の本質ではないのでこれは除いている)。

MySQL起動待ち対応

pytest-dockerのドキュメントではhttpbinのREST APIのコンテナーを立ち上げて、80ポートから200応答が帰るのを待つサンプルとして記述されている。MySQLの場合は下記のようにPyMySQLで接続しExceptionが帰ってこない、つまりDockerコンテナーが立ち上がり、初期データが投入完了したという判断にした。

def is_database_ready(docker_ip):
    try:
        pymysql.connect(
            host=docker_ip,
            user=os.getenv('MYSQL_USER', ''),
            password=os.getenv('MYSQL_PASSWORD', ''),
            db=os.getenv('MYSQL_DATABASE')
        )
        return True
    except:
        return False

上記をで0.1秒周期で30秒タイムアウトで待つ。

@pytest.fixture(scope="session")
def database_service(docker_ip, docker_services):
    docker_services.wait_until_responsive(
        timeout=30.0, pause=0.1, check=lambda: is_database_ready(docker_ip)
    )
    return

実行結果

Python環境の初期設定

$ # 実際の運用ではテスト環境と実行環境のためPipenvとかPoetryを使うのが良いかも
$ python -m venv .venv
$ source .venv/bin/activate
$ pip install -r requirements.txt

環境変数設定

$ cat << 'EOF' > .env
MYSQL_ROOT_PASSWORD='p@ssw0rd'
MYSQL_DATABASE='mydb'
MYSQL_USER='foo'
MYSQL_PASSWORD='pa$$Word'
EOF

テスト実行するとFixtureでDockerコンテナーを起動し、そのコンテナー内のdocker-entrypoint-initdb.dの機構によりSQLやシェルスクリプトなどが適用されてた後に実際のユニットテストが実行される。

$ pytest
============================= test session starts ==============================
platform darwin -- Python 3.8.6, pytest-6.1.2, py-1.9.0, pluggy-0.13.1 -- /Users/username/tmp/.venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/username/tmp, configfile: pytest.ini
plugins: dotenv-0.5.2, cov-2.10.1, docker-0.10.1
collected 1 item                                                               

tests/test_users.py::test_get_name_by_id PASSED                          [100%]

---------- coverage: platform darwin, python 3.8.6-final-0 -----------
Name         Stmts   Miss  Cover
--------------------------------
src/app.py       8      0   100%


============================== 1 passed in 16.35s ==============================

まとめ

  • pytest-dockerはデータベースに限らずテストに依存するサービスをDockerで立ち上げるられるので便利
  • docker-compose.ymlの配置位置をtests/からプロジェクト直下に変えると良いかも。通常のデバッグにも使える
  • docker_services.wait_until_responsive()にコンテナーの内容に合わせた起動待ち処理を実装する
0
3
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
3