\ ポイント最大11倍! /

【Django】ユニットテストを書く方法【単体テスト】

  • Django のテストコードってどう書けばいい??

このような疑問にお答えします。

ユニットテストは「単体テスト」とも呼ばれ、クラスや関数などの狭い範囲のプログラムが正常に動くか検証するものです。

僕は最初、「テストコードって書く必要ある?」くらいの印象でした。
というものテストコードなしでもプログラムは動きますし、動作に直接関係ないコードを書くことに抵抗があったからです。

ところが実務に入ってテストコードを書くと、以下に気づきました。

テストコードへの印象
  • エラー箇所がすぐわかる
  • 品質を一定に保てる
  • 一度書けばあとは自動でテストしてくれる
  • 何よりテストコードを書くのは楽しい!

テストコード、超重要です。

本記事では、テストコードを書いたことのない方向けに必要な知識をお伝えします。

テストコードを書く場所

Django でテストコードを書く場所は、次のいずれかを選択します。

  • 最初からある tests.py に書く
  • tests ディレクトリを掘ってモジュールを配置

小規模なプロジェクトなら前者で十分ですが、大規模なプロジェクトだと後者の方がコードが整理されてわかりやすいです。

最初からある tests.py に書く

以下はstartappコマンド実行直後のディレクトリ構成です。

プロジェクト
└── (省略)
アプリ
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── tests.py  # これに書きます!
└── views.py

一つ目の方法は、この tests.py に書いていく方法です。

テストケースの少ない開発であればこの方法で十分です。

tests ディレクトリを掘ってモジュールを配置

デフォルトで存在する tests.py を削除して、テスト用のディレクトリを掘る方法です。

プロジェクト
└── (省略)
アプリ
├── tests  # これを追加!
│   ├── __init__.py
│   └── test_models.py
├── views.py
├── __init__.py
├── admin.py
├── apps.py
└── models.py

テストモジュールを複数用意できるので、種類ごとにコードをまとめられてモジュール化できるのが利点。

この場合、以下の注意点を守りましょう。

  • __init__.py ファイルを配置する
  • ファイル名の先頭には test_ をつける

それぞれ、解説します。

__init__.py ファイルを配置する

Pyhton は__init__.pyがあるディレクトリをパッケージとして認識します。

つまり、このファイルがないとインポートができなくなってしまいます。

必ず配置しましょう。

内容は何も書かずに「空」の状態で OK です。

ファイル名の先頭には test_ をつける

Django は以下のような「ファイル名の先頭にtestとあるもの」をテストモジュールと認識します。

  • test_models.py
  • test_views.py
  • test_urls.py

先頭がtest以外だと、testモジュールとして認識されないので注意が必要です。

ファイル名は “test_テストするモジュール名” とすることが多いです。(例: test_models.py など)
ただし、大規模な開発ではもっと細かい粒度でモジュールを用意することもあります。(例: test_login.py など)

順を追って最初のテストコードを書こう!

以下の手順で、ごく簡単なテストコードを書いてみましょう。

  1. テストクラスを作成
  2. メソッドを定義する
  3. assert で結果を検証

それぞれ解説します。

テストクラスを作成

TestCaseを継承したテストクラスを定義します。

from django.test import TestCase


class TestModelsClass(TestCase):

TestCaseを継承すると各テストメソッドの実行前後でデータベースがリセットされ、テストが互いに干渉することを防ぎます。

メソッドを定義する

テストクラス内にテストコードを書くためのメソッドを用意します。

from django.test import TestCase


class TestSomeClass(TestCase):
    def test_is_valid(self):

メソッド名はtestで初めてください。

二つ以上のメソッドを用意した場合には、各メソッドは独立したテストケースとして機能することになります。

assert で結果を検証

最後に期待した結果が得られたかをassertメソッドで確かめます。

from django.test import TestCase


class TestSomeClass(TestCase):
    def test_is_valid(self):
        self.assertIs(0, 0)

上記はサンプルとしてself.assertIs(0, 0)としました。

第一引数と第二引数が等しい場合にはテスト合格、となります。

ちなみにassert系のメソッドは複数用意されているので、必要に応じて使い分けてください。

テスト結果を評価する assert メソッドは、以下のようなものがあります。

メソッド名内容
assertIs(a, b)a is b
assertEqual(a, b)a == b
assertNotEqual(a, b)a != b
assertTrue(x)x is True
assertFalse(x)x is False
assertIs(a, b)a is b
assertIsNot(a, b)a is not b
assertIsNone(x)x is None
assertIsNotNone(x)x is not None
assertIn(a, b)a in b
assertメソッド対応表

テストデータのセットアップとクリーンアップ

テストを行うために前提となるデータを用意しておきたい場合には、以下のような特別なメソッドを利用することがあります。

  • setUpメソッド
  • tearDownメソッド

それぞれ、解説します。

setUp メソッド

テストメソッド「実行前」に呼び出されるメソッドです。

テストの初期設定全般を行うメソッドで、テスト用データベースレコードの作成の他にも、テスト環境の初期設定などにも使われるものです。

class MyTestCase(TestCase):
    def setUp(self):
        # テスト用のユーザーを作成
        self.user = User.objects.create_user(
            username="testuser",
            password="12345"
        )
        # その他のセットアップコードが以下に続く

各テストメソッドの最初に実行されるので、テストコード内で何度も同じコードを繰り返し書く必要がなくなります。

tearDown メソッド

テストメソッド「実行後」に呼び出されるメソッドです。

そもそも Djangoの TestCase はデータベースに対する変更を自動的にロールバックするので、データベース関連のクリーンアップは不要です。

以下に、tearDownメソッドが必要になるケースを挙げます。

  • 外部ファイル、外部サービスを利用した場合
  • キャッシュや一時ファイルを削除する場合
  • 環境変数や設定を変更した場合
  • データベース以外のストレージを使った場合

以下にtearDownを使ったコード例を示します。

def tearDown(self):
    # テストで作成した一時ファイルを削除
    os.remove("path/to/temp/file")

    # 環境設定を元に戻す
    os.environ["MY_SETTING"] = self.old_setting

    # 外部リソースのセッションを終了
    self.external_resource.close()

テストコードの書き方

構成要素ごとにテストコードの書き方をご紹介します。

  • モデルのテスト
  • ビューのテスト
  • フォームのテスト

それぞれ解説していきます。

モデルのテスト

モデルのテストでは、「データ構造」と「ビジネスロジック」が期待通りのものかを検証します。

モデルのテストでは、以下のような内容になることが多いです。

  • Test Case クラスを継承してテストケースを作成
  • テスト内容を書くメソッドを定義
  • モデルインスタンスを作成
  • 期待通りの値が得られるか検証
  • カスタムメソッド、プロパティも検証
  • 保存、更新、削除などが正しく機能するか検証

具体的なコード例に起こすと、以下のようになります。

from django.test import TestCase
from myapp.models import MyModel

class MyModelTest(TestCase):
    def test_create_my_model(self):
        # インスタンスの生成
        model_instance = MyModel(name="Test Name")
        # インスタンスの保存
        model_instance.save()

        # データベースからインスタンスを取得
        saved_instance = MyModel.objects.get(name="Test Name")
        # 値の検証
        self.assertEqual(saved_instance.name, "Test Name")

    def test_model_method(self):
        # モデルメソッドのテスト
        model_instance = MyModel(name="Test")
        self.assertEqual(model_instance.get_name(), "Test")

ビューのテスト

リクエスト処理とレスポンス生成が期待通りかを検証します。

ステップとしては以下の通りです。

  • テストケースクラスを作成
  • クライアントの設定
  • ビューへのリクエスト送信
  • レスポンスの検証
  • テンプレートとコンテキストの検証

以下にビューのテストコード例を示します。

from django.test import TestCase
from django.urls import reverse

class ViewTest(TestCase):
    def test_index_view(self):
        # ビューへのリクエストをシミュレート
        response = self.client.get(reverse("index"))

        # レスポンスのステータスコードが200であることを確認
        self.assertEqual(response.status_code, 200)

        # 正しいテンプレートが使用されているかを確認
        self.assertTemplateUsed(response, "index.html")

        # レスポンスに特定のテキストが含まれているかを確認
        self.assertContains(response, "Welcome to the Index Page")

ビューにアクセスして検証する様子をコードで書き起こしている形になります。

フォームのテスト

バリデーション、フィールドのデフォルト値、保存ロジックを検証します。

以下がフォームテストの流れです。

  • フォームバリデーションのテスト
  • フィールドのデフォルト値の動作テスト
  • フォームの送信と保存テスト

具体的なコード例でも確認しましょう。

from django.test import TestCase
from myapp.forms import MyForm

class FormTestCase(TestCase):
    def test_form_validation(self):
        form_data = {"name": "test", "age": 20}
        form = MyForm(data=form_data)
        self.assertTrue(form.is_valid())

        form_data = {"name": "", "age": 20}
        form = MyForm(data=form_data)
        self.assertFalse(form.is_valid())
        self.assertIn("name", form.errors)

Visual Studio Code で自動テストができない場合

Visual Studio Code ではフラスコのマークから自動テスト実行ができます。

ところが以下のエラーが発生してしまいました。

Pytest Discovery Error
Error discovering pytest tests (see Output > Python):

原因は以下の3パターンがあるようです。

  • __init__.py がない
  • ライブラリの不足
  • リロードが必要になっている

一つずつ試してみてください。

僕の場合は「リロード」で解決できました。

__init__.py がない

自分でテスト用ディレクトリを作った場合、ディレクトリ配下に __init__.py の生成を忘れていないか確認してみてください。

このファイルがない場合は Python の処理対象から外れてしまいます。

__init__.py はただ「あれば」よくて、中身は空白でOKです。

ライブラリの不足

テストコードで必要なライブラリがインストールされていない場合も、このエラーが発生するようです。

お使いのパッケージ管理システム(pip, Poetry など)で、不足分のライブラリをインストールしましょう。

リロードが必要になっている

何らかの原因で、 VisualStudioCode のリロードが必要になっているケースです。

F1 > Reload Window と入力

原因1と原因2でもうまくいかない場合はリロードを試してみるのが良さそうです。

この記事が気に入ったら
フォローしてね!

シェア・記事の保存はこちら!

この記事を書いた人

karo@プログラマのアバター karo@プログラマ プログラマ

「書くことで人の役にたつ」をモットーに活動中。
本職はプログラマで、Pythonが得意。
基本情報技術者試験合格。

コメント

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)