- 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
があるディレクトリをパッケージとして認識します。
つまり、このファイルがないとインポートができなくなってしまいます。
必ず配置しましょう。
ファイル名の先頭には test_ をつける
Django は以下のような「ファイル名の先頭にtest
とあるもの」をテストモジュールと認識します。
- test_models.py
- test_views.py
- test_urls.py
先頭がtest
以外だと、testモジュールとして認識されないので注意が必要です。
順を追って最初のテストコードを書こう!
以下の手順で、ごく簡単なテストコードを書いてみましょう。
- テストクラスを作成
- メソッドを定義する
- 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 |
テストデータのセットアップとクリーンアップ
テストを行うために前提となるデータを用意しておきたい場合には、以下のような特別なメソッドを利用することがあります。
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 の処理対象から外れてしまいます。
ライブラリの不足
テストコードで必要なライブラリがインストールされていない場合も、このエラーが発生するようです。
お使いのパッケージ管理システム(pip, Poetry など)で、不足分のライブラリをインストールしましょう。
リロードが必要になっている
何らかの原因で、 VisualStudioCode のリロードが必要になっているケースです。
F1
> Reload Window
と入力
原因1と原因2でもうまくいかない場合はリロードを試してみるのが良さそうです。
コメント