DjangoでUnitTestを実装する方法

2021年06月09日

Djangoでテストを実装します。

はじめに

Djangoには標準でテストランナーがバンドルされており、デフォルトのまますぐにテストを実装&実行できるようになっています。

  • テスト実行コマンド
  • テスト用DBの自動作成・破棄
  • テストfixture
  • テスト用リクエストClient
Webアプリのテストコード実装に必要なヘルパーユーティリティがほぼ完備されています。

テスト実行コマンド

$ python manage.py test

プロジェクト配下に存在する django.test.TestCase を継承したテストクラスをスキャンして実行してくれます。

テストを記述するファイル名は「test」はじまりとする必要があります。

テスト対象コード

テスト対象コード
# model
class Todo(models.Model):
    memo = models.TextField()
    is_done = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

# view
class IndexView(View):
    def get(self, request):
        todos = Todo.objects.all().order_by('-updated_at')
        return render(request, 'todoapp/index.html', {
            'todos': todos
        })

データベースから情報を抽出して画面表示を行うviewをテスト対象とします。

todoapp/tests.py
from django.test import TestCase


class TestIndexView(TestCase):
    def test_index_get(self):
        '''DBが空の場合'''
        # Viewに対してルートパスをurls.pyで割り当てしている
        response = self.client.get('/')

        # HTTPステータス:成功
        self.assertEqual(response.status_code, 200)

        # QuerySet:空
        self.assertQuerysetEqual(
            response.context['todos'],
            []
        )

まずはテストデータなしで上記のテストコードを実行してみます。response.contextにてViewからTemplateに渡る変数群を参照できます。ViewでDBからデータを取得して、「todo」のキー名でQuerySetをResponseContextに渡しているので、この内容が空であることを検査します。

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.028s

OK
Destroying test database for alias 'default'...

テストが通りました。

テスト用DBの自動作成・破棄

テスト実行ログをよく見ると分かりますが、テスト実行時はDjangoのテストランナーが本番DBと同じスキーマで別のDBを自動作成してくれています。開発デバッグ時に参照するDBではなく、同じ内容をコピーしたDBに向き先を勝手に変えてくれます。全てのテストが完走したら自動でテストDBを削除してくれます。とてもエンジニアフレンドリーな設計になってますね!

テストデータを絡めたテスト実装方法

空データのテストだけでは実用的なテストになりませんね。

次はDBにデータがある状態のテストを書いてみましょう。

方法は2つあります。

  • fixtureを使う
  • テストクラスのsetUp時にModelを使ってデータ生成する

fixtureを使うと楽できますが、今回は後者のModelを使って自力でテストデータ投入したいと思います。

todoapp/tests.py
from todoapp.models import Todo


class TestIndexViewWithData(TestCase):

    @classmethod
    def setUpTestData(self):
        # DBに1件データを作成する
        Todo.objects.create(
            memo='テストデータ',
        )

    def test_index_get_with_data(self):
        '''DBにデータが1件入っているテスト'''
        response = self.client.get('/')

        todos = response.context['todos']

        # 1件であること
        self.assertEqual(len(todos), 1)

        # テストデータが正しいこと
        self.assertEqual(todos[0].memo, 'テストデータ')

新しいテストケースを定義しました。setUpTestData()がテストDBにデータを永続化するタイミングとなります。Viewの実装はtodoテーブルから全件読み出してテンプレートに渡しているので、件数が1件であることと、読みだしたデータが正しいことをチェックしています。

テストデータを入れてテストするのも簡単ですね!

まとめ

今回はDjangoのテストに触れてみました。ヘルパー機能がかなり良い感じに整備されており、テストコードの実装にモチベーション高く取り組めます。

過去に現場で構築したシステムだと、DBはマルチクラスタ構成、他の副作用ポイントとしてRedisや外部サービスAPI連携などがあり、Mockingやテストコード実行環境構築の適切な運用設計をしなければなりませんでしたが、Djangoは柔軟に対応できるフレームワークだったのでキチンと運用に乗せることができました。

では、今回はここまで。

良きPythonライフを!


Web系エンジニアでPython好き。バックエンド/フロントエンド問わずマルチな方面でエンジニアリングしています。