エイエイレトリック

なぐりがき

# Django と DRF の TestCase を使いこなす

Django Advent Calendar 2023 の12日目の記事です。

前日は @ryu22e さんの Django 5.0 主な変更点まとめ #Python - Qiita でした。

問題意識

普段から DjangoDjango REST Framework (DRF) を使っているのですが、テストケースを書く際、いろんな TestCase クラスの選択肢があり、いつもその場のノリで選んでしまっています。

この場を借りてどのクラスを使うのがベストなのか考えます。

// 動作確認に利用したパッケージ
Django              4.1.2
djangorestframework 3.14.0

いろんな TestCase

Django もしくは DRF でクラスベースのテストを書く際の選択肢は以下です

下の2つは Python 標準ライブラリを継承したクラスであることは予想できます。 実際にどう違うのか調べます。

unittest.TestCase

Django に限らず、 Pythonユニットテストで利用できるクラスです。

self.assertHogehoge というアサートメソッドを持っています。

django.test.TestCase

unittest.TestCase を継承したDjango のテストケースクラスですが、実は直接継承しているわけではありません。

https://docs.djangoproject.com/en/5.0/topics/testing/tools/#provided-test-case-classes

つまり、 DjangoTestCaseSimpleTestCaseTransactionTestCase を継承したクラスです。

ドキュメント ではこう記述しています。

あなたの Django アプリケーションがデータベースを使用しない場合は、SimpleTestCase を使ってください。

特定のデータベーストランザクションの振る舞いをテストしたい場合は、TransactionTestCase を使ってください。

データベースを使い、かつトランザクション関連のテストが必要ない場合は django.test.TestCase を使うのがよさそうです。

unittest.TestCase or SimpleTestCase

データベースを使わないテストは SimpleTestCase とあるが、unittest.TestCase でいいのでは?と思ったので差分を確認します。

該当コード (GitHub)

  • Client クラスを扱える: 初期化時に self.client を設定
    • エンドポイントへのリクエストに利用
  • Field クラスや response のための アサーション

アサーションは特殊な関数が多いので、 Client クラスが Django のテストクラスにとって重要な存在と言えそうです。

Viewクラスに対する簡単なテストケースを書いてみました。 unittest.TestCase だと初期化関数 (setUp) で self.client の設定が必要ですが、 SimpleTestCase では不要です。

import unittest
from django.test import SimpleTestCase
from django.test.client import Client

# python 標準
class TestConvertViewUnittest(unittest.TestCase):
    def setUp(self):
        self.client = Client()

    def test_get_ok(self):
        response = self.client.get("/api/converter/", data={"text": "test"})
        self.assertEqual(response.status_code, 200)

# Django
class TestConvertViewSimple(SimpleTestCase):
    def test_get_ok(self):
        response = self.client.get("/api/converter/", data={"text": "test"})
        self.assertEqual(response.status_code, 200)
        # SimpleTestCase のアサーション
        # status と レスポンスのテキストを同時にアサート
        self.assertContains(response, text="ティーイーエスティー", status_code=200)
        # response.json() とすれば assertDict で代替可能
        self.assertJSONEqual(response.content, {"text": "ティーイーエスティー"})

rest_framework.test.APITestCase

DRF のテストクラスは Django のテストクラスと対応しています。

  • SimpleTestCase -> APISimpleTestCase
  • TransactionTestCase -> APITransactionTestCase
  • TestCase -> APITestCase

Django のテストクラスとの差は主に client に APIClient が設定されている部分です。

REST framework includes the following test case classes, that mirror the existing Django's test case classes, but use APIClient instead of Django's default Client. 該当コード (GitHub)

APIClient

APIClient は基本的には Django の Client を継承したクラスです。

django.SimpleTestCase の場合は データベースを利用しないため特に影響はありません。

class TestConvertViewDRF(APISimpleTestCase):
    def test_get_ok(self):
        response = self.client.get("/api/converter/", data={"text": "test"})
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, text="ティーイーエスティー", status_code=200)
        self.assertJSONEqual(response.content, {"text": "ティーイーエスティー"})

データベースを利用し、 User モデル による認証が必要な場合、 APIClient が役に立ちます。

force_authenticate でリクエストを強制的に実行できるからです。

以下のように authentication_classespermission_classes が設定されているViewクラスを例にテストクラスを考えます。

# view.py
from rest_framework.generics import RetrieveAPIView
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
from rest_framework.permissions import IsAuthenticated


class AlphabetView(RetrieveAPIView):
    serializer_class = AlphabetRequestSerializer
    authentication_classes = [BasicAuthentication, SessionAuthentication]
    permission_classes = [IsAuthenticated]

データベースを利用するため Simple なしのクラス APITestCase を使います。

認証用のユーザーを作成し、 リクエストを実行前に force_authenticate で作成したユーザーを渡すことで認証が可能になります。

from django.contrib.auth import get_user_model
from rest_framework.test import APITestCase


class TestAlphabetViewDRF(APITestCase):
    def setUp(self):
        #  get_user_model は django.contrib.auth.models.User と同じ
        self.user = get_user_model().objects.create_user(username="test_user")

    def test_get_ok(self):
        self.client.force_authenticate(user=self.user)
        response = self.client.get("/api/alphabet/")
        self.assertEqual(response.status_code, 200)

    def test_get_ng(self):
        # 認証なし
        response = self.client.get("/api/alphabet/")
        self.assertEqual(response.status_code, 401)

まとめ

  • Django のテストケースクラスは self.client を使ってリクエストのテストができる
    • データベースを使わないときは SimpleTestCase
    • 使う場合は TestCase
  • DRF のテストケースクラスは Django のとほぼ同じ
    • 認証が必要な場合の設定は APITestCase を使うと楽 (かもしれない)

参考資料