Edited at

[Python] pytest + pymysql による実際の database を使ったユニットテスト


概要

実際のDBを使った CRUD のユニットテスト


(簡易)自己紹介

ペチパーです。


環境情報


  • mac

  • python 3.7.3


    • pytest

    • pymysql

    • python-dotenv



  • invoke (タスクランナー)


レポジトリ

今回のやつをまとめたレポジトリ

https://github.com/mentol310/test_sql


実際のデータベースを使ったユニットテストの流れ


  1. 実データに影響のないよう、テスト用データベースでテストする

  2. 何回テストを繰り返しても大丈夫なように



    1. rollback 処理を入れる


    2. drop if exists で存在する場合削除するようにする ... etc



  3. 本利用の場合は、実データ相当のシーダーを用意することになりそう


下準備

テスト用データベースの用意とユーザーの用意(権限の付与)


データベース接続〜テーブル生成

作成直後はレコードが空であることを確認。

# phpunit で言うところの setup, teardown は fixture を利用する。

@pytest.fixture(scope="module")
def conn():
# setup
conn = pymysql.connect(
host="localhost",
user=USER,
password=PASSWORD,
db="test_app",
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor
)
# yield で指定したオブジェクトが返る
yield conn
# teardown
conn.close()

@pytest.fixture
def cursor(conn):
cursor = conn.cursor()
yield cursor
conn.rollback()

@pytest.fixture
def initialize(cursor):
stmt = textwrap.dedent("""
CREATE TABLE users (
id int,
name varchar(20)
)
"""
)
cursor.execute(stmt)

def test_created(cursor, initialize):
cursor.execute("SELECT * FROM users")
result = cursor.fetchall()
assert dumps(result) == "[]"

print デバッグなどをつけると fixture の流れが分かりやすかったです。

---------------------------- Captured stdout setup -----------------------------

start conn
conn cursor
drop users
start create
--------------------------- Captured stdout teardown ---------------------------
rollback conn


インサート

テストメソッドの引数として、複数の fixture を扱うこともできるみたいで便利 :raised_hands:

@pytest.fixture

def insert_users_seed(cursor):
print("insert users")
stmt = textwrap.dedent("""
INSERT INTO users
VALUES (1, 'foo'), (2, 'bar'), (3, 'hoge')
""")
cursor.execute(stmt)

def test_insert(cursor, insert_users_seed):
cursor.execute("SELECT * FROM users")
result = cursor.fetchall()
assert dumps(result) == dumps([
{"id": 1, "name": "foo"},
{"id": 2, "name": "bar"},
{"id": 3, "name": "hoge"},
])


アップデート

インサートを呼んでインサートの結果を引き継いで利用できていることを確認できています。

@pytest.fixture

def update_users(cursor, insert_users_seed):
print("update users")
stmt = textwrap.dedent("""
UPDATE users SET name = 'barbar' WHERE id = 2
"""
)
cursor.execute(stmt)

def test_update(cursor, update_users):
cursor.execute("SELECT * FROM users")
result = cursor.fetchall()
assert dumps(result) == dumps([
{"id": 1, "name": "foo"},
{"id": 2, "name": "barbar"},
{"id": 3, "name": "hoge"},
])


デリート

同上

@pytest.fixture

def delete_users(cursor, update_users):
print("delete users")
stmt = textwrap.dedent("""
DELETE FROM users WHERE id = 3
"""
)
cursor.execute(stmt)

def test_delete(cursor, delete_users):
cursor.execute("SELECT * FROM users")
result = cursor.fetchall()
assert dumps(result) == dumps([
{"id": 1, "name": "foo"},
{"id": 2, "name": "barbar"},
])


最後に


  • pytest 相変わらず便利

  • タスクランナー invoke いい感じ

  • 小さいツールなら docker じゃなくてもローカルでいいかも(今更)

  • 普通の大きいサービスでこれやると(いろんな意味で)死にそう

  • 小さいDB使ったツールのテストにはガンガン使っていけそう


参考リンク