【Django】Django TestCaseでモデル・ビューのテストを書く

Django

こんにちは、かつコーチです。

「動くには動いているけど、テストは書いたことがない」というDjango学習者は少なくありません。
筆者自身も最初の頃は、機能を作っては手動でブラウザを操作して確認する、という進め方をしていました。

しかしアプリが大きくなるほど、手動確認では「直したつもりが別の機能を壊していた」という事故が増えていきます。
この記事では、Django標準のTestCaseを使ってモデルとビューのテストを書く基本を解説します。

Djangoのテストとは?

TestCaseが自動でやってくれること

Djangoのdjango.test.TestCaseは、Python標準のunittestを拡張したクラスです。
トランザクション(一連のDB操作をひとまとまりにする仕組み)を使い、各テストメソッドの実行後に自動でデータベースをロールバックしてくれます。

つまり、あるテストで作成したデータが別のテストに影響を与える心配がありません。
この仕組みのおかげで、開発者はテスト用データの後片付けを意識せずにテストを書けます。

なぜテストを書く必要があるのか

「動作確認はブラウザで目視すればいい」と考えがちですが、それでは以下のような問題が起きます。

  • 機能追加のたびに、過去の機能もすべて手動で再確認するのは現実的でない
  • 確認漏れによって、リリース後にバグが発覚する
  • 仕様変更の影響範囲を、コードを読むだけでは把握しきれない

一度テストを書いておけば、python manage.py testを実行するだけで既存機能が壊れていないかを数秒〜数十秒で確認できます。
この「安心して変更できる状態」を作ることが、テストを書く一番の目的です。

モデル・ビューのテストを書く手順

テストファイルの配置とテストクラスの基本形

startappでアプリを作ると、自動的にtests.pyが生成されます。
規模が大きくなる場合はtests/ディレクトリに分割し、test_models.pytest_views.pyのようにファイルを分けるのが一般的です。

# blog/tests/test_models.py
from django.test import TestCase
from blog.models import Article


class ArticleModelTest(TestCase):
    def setUp(self):
        """各テストメソッドの実行前に呼ばれる共通の準備処理"""
        self.article = Article.objects.create(
            title="テスト記事",
            body="本文です",
        )

    def test_str_returns_title(self):
        """__str__メソッドがタイトルを返すことを確認する"""
        self.assertEqual(str(self.article), "テスト記事")

    def test_default_is_published_false(self):
        """デフォルトで非公開状態になっていることを確認する"""
        self.assertFalse(self.article.is_published)

ビューのテストではClientを使う

ビューのテストには、self.client(テスト用の疑似ブラウザ)を使ってリクエストを送ります。

# blog/tests/test_views.py
from django.test import TestCase
from django.urls import reverse
from blog.models import Article


class ArticleListViewTest(TestCase):
    def setUp(self):
        Article.objects.create(title="公開済み記事", is_published=True)
        Article.objects.create(title="下書き記事", is_published=False)

    def test_status_code_200(self):
        response = self.client.get(reverse("blog:article_list"))
        self.assertEqual(response.status_code, 200)

    def test_only_published_articles_are_shown(self):
        response = self.client.get(reverse("blog:article_list"))
        self.assertContains(response, "公開済み記事")
        self.assertNotContains(response, "下書き記事")

reverse()でURL名からパスを解決しているのがポイントです。
URLパターンを直接文字列で書かないことで、urls.pyの変更に強いテストになります。

認証が必要なビューのテスト

ログインが必要なページをテストする場合は、create_userとログイン処理を組み合わせます。

from django.contrib.auth import get_user_model

User = get_user_model()


class MyPageViewTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(username="taro", password="testpass123")

    def test_redirect_if_not_logged_in(self):
        response = self.client.get(reverse("blog:mypage"))
        self.assertEqual(response.status_code, 302)

    def test_ok_if_logged_in(self):
        self.client.login(username="taro", password="testpass123")
        response = self.client.get(reverse("blog:mypage"))
        self.assertEqual(response.status_code, 200)

get_user_model()を使うことで、標準のUserモデルでもカスタムユーザーモデルでも同じコードが使い回せます。

よくあるつまずきポイント・エラー対処

❌ Before:本番用データベースの設定でテストを実行してしまう

筆者が最初にテストを書いたとき、DATABASESの設定を1つしか用意しておらず、テスト実行時に本番用DBの認証情報を使おうとしてエラーになったことがあります。

django.db.utils.OperationalError: could not connect to server: Connection refused

原因は、テスト実行時にDjangoが「テスト専用のデータベース」を自動作成しようとする際、接続先の設定(ホストや権限)が本番環境向けのままだったことでした。

✅ After:settings.pyのDATABASESをテスト用に見直す

開発環境用のsettings.pyでは、ローカルのSQLiteやテスト用ユーザー権限を持ったPostgreSQLを指すように設定を分けます。

# settings/local.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

テストは基本的にローカル環境や、settings/testing.pyのようにテスト専用の設定ファイルを分けて実行するのが安全です。
本番相当のDB接続情報を使ってテストを実行することは避けましょう。

応用・一歩先の使い方

assertメソッドを使い分けて可読性を上げる

TestCaseにはassertEqual以外にも便利なアサーションメソッドが揃っています。

メソッド用途
assertContains(response, text)レスポンス本文に特定の文字列が含まれるか
assertRedirects(response, url)期待通りのURLへリダイレクトされたか
assertQuerySetEqual(qs, list)QuerySetの中身が期待通りか
assertFormError(response, form, field, error)フォームのバリデーションエラー内容が正しいか

用途に合ったアサーションを選ぶことで、テストが失敗したときのエラーメッセージも分かりやすくなります。

カバレッジを計測して抜け漏れを可視化する

coverage.pyを導入すると、どのコードがテストで実行されていないかを可視化できます。

pip install coverage
coverage run --source='.' manage.py test
coverage report

100%を目指す必要はありませんが、重要なビジネスロジックのカバレッジが低い箇所を見つける手がかりになります。

まとめ

この記事のポイント

  • TestCaseはテストごとにDBを自動でロールバックしてくれるため、後片付けを意識しなくてよい
  • モデルのテストはsetUpでデータを用意し、assertEqualなどで検証する
  • ビューのテストはself.clientreverse()を組み合わせる
  • 認証付きビューはget_user_model()self.client.login()でテストする
  • テスト用のデータベース設定を本番用と分けておく

次に読むべき記事

TestCaseの基本に慣れたら、より書きやすいテストフレームワークにも触れてみましょう。
「pytest-djangoでテストをもっと書きやすくする」の記事で、pytest-djangoとの違いを比較しています。

タグ: Django, 中級者向け, テスト

タイトルとURLをコピーしました