pytest_Djangoのモデルを使ったAPIのテストを書く_pytest, mocker #341日目
pytestの設計手順を以前まとめましたが、今回は具体的な実装部分についてメモしておきます。
事前準備として、該当APIを叩いた時のレスポンス内容を確認しておく必要があります。DRFを使っている場合、Serializerを確認すれば最終的なレスポンスにどういうフィールドが含まれているか分かります。
pytestのコードは大きく以下の流れです
①テスト用のデータを準備
②テスト用のデータベースを①から生成
③そのAPIを叩いた時に期待される結果(正解データ)を定義(expect)
④本番と同じAPIを②に対して実行
⑤どういう結果だったらOKとするか定義(assert)
以下では、Store(店舗)単位で顧客と販売履歴を管理している前提で、顧客リストを参照するAPIをテストしています。まずテスト用データを作ります。以下のイメージでJSON形式のデータを用意してfixturesに格納します。
[app/tests/fixtures/mysql/json/customer.json]
[
{
"id": 1,
"name": "Aさん",
},
{
"id": 2,
"name": "Bさん",
},
]
[app/tests/fixtures/mysql/json/goods.json]
[
{
"id": 1,
"goods_name": "ポスター",
"price": 100,
},
{
"id": 2,
"goods_name": "シール",
"price": 50,
},
]
[app/tests/fixtures/mysql/json/purchase_history.json]
[
{
"date": "2023-01-21",
"name_id": 1
"goods_id": 1,
"quantity": 3,
},
{
"date": "2023-01-22",
"name_id": 2
"goods_id": 2,
"quantity": 7,
},
]
上記をテスト用のデータベースに格納する処理の定義です。JSONファイルのパスを受け取って、データを各Modelに格納していきます。3つ同じような定義なのでcustomerのみ記載します。
[app/tests/fixtures/mysql/create_customer.py]
from app.models import Customer
@pytest.mark.django.db(databases=["default"])
def customers_from_json(json_path: str):
with open(json_path) as f:
json_data = json.load(f)
customers: List[Customer] = []
for j in json_data:
customer = Customer(
id = j['id'],
name = j['name'],
)
customers.append(cm)
Customer.objects.bulk_create(customers)
@pytest.mark.django_db(databases=['default'])
def reset():
Customer.objects.all().delete()
肝心のテスト部分は以下です。終盤に少し解説を入れています。
[app.tests.views.test_customer_list_view.py]
from operator import itemgetter
import pytest
from rest_framework.test import APIClient
from app.models import Store
from app.tests.fixtures.mysql import (
create_customer,
create_goods,
create_purchase_history,
)
class TestCustomerList:
def setup_method(self):
# 各ユニットテストの開始前に実行したい処理を記載
# テスト用のデータベースなど、関数毎に独立していて関数間でデータが保持されないもの等を定義
create_customer.customers_from_json('app/tests/fixtures/mysql/json/customer.json')
create_goods.goods_from_json('app/tests/fixtures/mysql/json/goods.json')
create_purchase_history.purchase_history_from_json('app/tests/fixtures/mysql/json/purchase_history.json')
@classmethod
def setup_class(cls):
# テスト全体で開始前に1度だけ実行したい処理を記載
# テスト用のキャッシュなど、関数間でデータが保持できるもの等を定義
def teardown_method(self):
# 各ユニットテストの終了後に実行したい処理を記載
# テスト用のデータベースのリセットなど
create_customer.reset()
create_goods.reset()
create_purchase_history.reset()
@classmethod
def teardown_class(cls):
# テスト全体で終了後に1度だけ実行したい処理を記載
# テスト用のキャッシュのリセットなど
@staticmethod
def mock_xxxx_property(store: Store):
# 後述するpytest.mark.django_dbでもテスト用DBからデータ抽出できない共通のケースがある場合、
staticmethodでメソッド化しておく
# プロダクションコード上でSQLを直に記載している場所など
@pytest.mark.django_db
def test_post_customer_list(self, mocker):
store_id = 1
store = Store.object.get(id=store_id)
# モックが必要な処理はここで対応
mocker.patch(
"モックしたい処理のパス", # 記載例: app.models.Store.stores *最後のstoresはStoreモデルのproperty
new_callable = mocker.PropertyMock, # propertyに関してはこの指定が必要(property以外なら不要)
return_value = self.mock_xxxx_property(store)
)
url = 'テストしたいAPIのURL'
params = {クエリパラメーターがあれば記載}
response = APIClient().post(url, data=params, format="json", **create_auth.user_header(必要あれば認証情報を渡す))
# エラーにならずにレスポンスが帰るかテスト
assert response.status_code == 200
# レスポンス内容をJSONに変換
r_json = response.json()
# 正解データをJSON形式で定義(この通りにレスポンスしてきたらテスト成功)
expect = [
{
正解データA
},
{
正解データB
},
{
正解データC
}
]
# データ数が一致しているかテスト
assert = len(r_json) == len(expect)
# idで並べ替えて、データが完全一致するかテスト
assert = sorted(r_json, key=itemgetter("id")) == sorted(expect, key=itemgetter("id"))
pytestに標準装備されているメソッド
setup_method :各ユニットテストの開始前に実行
teardown_method:各ユニットテストの終了後に実行
setup_class :テスト全体の開始前に実行
teardown_class:テスト全体の終了後に実行
@pytest.mark.django_db
pytestでデータベースへのアクセスが必要な場合に使用します。テスト用のデータベースはテスト用として独立しており、プロダクション側のデータベースには影響を与えません。また、関数毎にも独立しており、各テストが完了する度に破棄されます。
mocker
pytest-mockをインストールすると使用可能になります。差し替えたい処理のパスをpatchで指定してモックにできます。SQLを直書きしている処理などはテスト用DBにアクセスできないので、ここでモックすればOKです。
ここまでお読みいただきありがとうございました!
参考
この記事が気に入ったらサポートをしてみませんか?