見出し画像

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です。


ここまでお読みいただきありがとうございました!


参考


この記事が気に入ったらサポートをしてみませんか?