matobaの備忘録

育児しながら働くあるエンジニアの記録

pytestでdjangoのmodelsとviewをテストする

pytestでdjangoのmodelsをテストする方法をよく忘れるなあ、と思ってます。

新規でコードを書くより、バグ調査したり、レビューしてることが多くなると、いざ書こうとした時に「さて、どうやるんだっけな」的な感じになる。

というわけで書いときます。

まず、pytestでdjangoをテストする話

とりあえず、前にpytestとdjangoの話について以下に書きました。

blog.mtb-production.info

でも、上記ではdjangoのカバレッジを図っただけで、pytestからmodelsとかviewをテストする方法を書いてなかったです。

とりあえず、djangoをpytestするにはpytest-djangoを入れる必要があります。

入れた後は、pytest.iniあたりに以下の記載をするとdjangoをpytestでテストできます。(たぶん)

DJANGO_SETTINGS_MODULE = project.settings_test

最新情報はドキュメントを見てください。

djangoのmodelsをテストする

pytestからdjangoのmodelsをテストするときは、 pytest.mark.django_db を使います。

例えば、こういうmodels app/models.pyMyAppModel があったとして、

from django.db import models

class MyAppModel(models.Model):
    title = models.CharField(max_length=20)
    content = models.CharField(max_length=255, default="")

    @property
    def is_empty(self):
        return not bool(self.content)

こんな風にテストできます。

import pytest

from app.models import MyAppModel


@pytest.mark.django_db
def test_app_is_empty():
    mymodel = MyAppModel(title="sample model")
    assert mymodel.is_empty

@pytest.mark.django_db
def test_app_isnot_emmpty():
    mymodel = MyAppModel(title="sample model", content="test")
    assert not mymodel.is_empty

このpytestのコードを見せると、「いや、もっと綺麗な書き方がある」「サンプルが微妙」とかツッコミたい人がいるかもしれませんが、とりあえずおいといてください。

pytest.mark.django_db というデコデータをつけるとそのテストで、Djangoのテスト用DBが使えるようになります。

Django helpers — pytest-django documentation

pytestでDjangoのViewをテストする

テスト対象の準備

とりあえずテスト対象として、urlsがこんな感じ。

from django.urls import path

from app import views as app_views

urlpatterns = [
    path('^', app_views.index_view),
]

app/views.py がこんな感じ。

from django.http import HttpResponse

def index_view(request):
    return HttpResponse("Hello World")

テストする

テストは二つの方法がある。djangoのTestClientを使う場合と、djangoのRequestFactoryを使う場合。基本的にpytest-djangoのドキュメントに書いてある話と同じだけど、改めて説明すると以下。

djangoのTestClientを使う場合はこんな感じ。

def test_app_view(client):
    response = client.get('/')
    assert response.status_code == 200

djangoのRequestFactoryを使う場合はこんな感じ。

def test_app_view(rf):
    request = rf.get('/')
    response = index_view(request)
    assert response.status_code == 200

djangoのDBを使わない場合は pytest.mark.django_db というデコデータは不要。

ちなみに、TestClientなのかRequestFactoryなのかは、test関数の引数が何か、によって判定されてる。

client という引数が渡ってくる想定で書くとDjangoのTestClientが渡ってくるし、rf という引数が渡ってくる想定で書くとDjangoのRequestFactoryが渡ってくる。

引数が渡ってくる仕組み

最初に見たとき、test関数に渡ってくるclientとかrfがよくわからなかった。なんで引数渡ってくるんだろ、とか思ってた。

正直、pytestがよくわかってなくて雰囲気で触ってたので、それが原因だった。

簡単にいうとpytestのfixtureという機能が関係している。

pytestのfixture

簡単にいうと、「データを作ってテスト関数に渡す仕組み」がpytest fixture

例えば、こういうテストケースがあったとして、

import pytest

from app.models import MyAppModel

@pytest.mark.django_db
def test_app_is_empty():
    mymodel = MyAppModel(title="sample model")
    assert mymodel.is_empty

@pytest.mark.django_db
def test_app_isnot_emmpty():
    mymodel = MyAppModel(title="sample model", content="test")
    assert not mymodel.is_empty

このように書き換えられる。

import pytest

@pytest.fixture
def test_model():
    from app.models import MyAppModel
    return MyAppModel

@pytest.mark.django_db
def test_app_is_empty(test_model):
    mymodel = test_model(title="sample model")
    assert mymodel.is_empty


@pytest.mark.django_db
def test_app_isnot_emmpty(test_model):
    mymodel = test_model(title="sample model", content="test")
    assert not mymodel.is_empty

上記の例の場合は、 MyAppModel のimportのタイミングがモジュールのimportのタイミングではなくなったのが嬉しい。

このようなfixtureをpytest-djangoの中で作成しているからrfやclientが渡ってくる。

pytest-djangoの仕組み

上記の理解を含めて、pytest-djangoのコードを読んでみる。

この辺にある。

github.com

まあ、量が多いわけではないので、とりあえずざっと眺めて見た。

  • pytest_django/fixtures.py あたりに、 rf とか client のfixture定義がある。
  • pytest_django/plugin.pyfrom .fixtures import rf # noqa とかの定義がある。
  • pytest のpluginの探し方をみると、ライブラリをインストールする際の entry_pointを見る。

  • pytest-djangosetup.py の中に pytest11 というentry_pointがあり、 pytest_django.plugin につながっている。

はい。

というわけで、pytest_djangoはこんな風にclientやrfを読み込む。

終わり

途中で少し脱線しましたが、とりあえずpytestでdjangoのmodelsとviewをテストする話を書きました。

pytestは奥が深いので持って、いろいろ触っていきたいなー

ではでは。