8
9

【Django】テストってなんぞ!٩(๑`^´๑)۶な方へ

Last updated at Posted at 2022-10-04

テストコードにチャレンジしよう

Djangoで独自のアプリを開発していると、テストについて考えるようになってくると思います。

「テストコードがない=品質に保証がない」となる場合があるので、簡単なテストだけでも書けるようになっておくと良いです。

テストコードは、クライアントや第三者のためにあると思われるかもしれませんが、開発者にも大きなメリットがあります。

テスト無しの場合

  1. コーディングする(runserverでエラーチェック)
  2. http://127.0.0.1:8000/ページや関数の数だけ手動で動作チェック
  3. ページやviewが多いと面倒くさい

テスト有りの場合

  1. コーディングする(runserverでエラーチェック)
  2. python manage.py test全てのページと関数を自動でチェック
  3. ページやviewが多くなっても楽ちん

あとrunserverで表示されないエラーも表示されちゃう!

知っておくこと

  • テストコードがアプリケーションのコードよりも大きくなってしまう場合がある。
  • テストコードは多いほど良い。
  • テストコードは美しくなくても良い。

アプリケーションのコードより、テストコードが大きくなってしまう事は良くあることらしく、それほど重要で便利なものとして認識されています。

どこからテストしようか?

  • 何をテストすればいいの?
  • コードはどう書けばいいの?

みたいな疑問が沢山あり、初めてのテストに踏み込めない方もいると思いますが、全てテストしようとすると気が滅入るので、まずは一つずつやっていきましょう。

アプリケーションを完成させてからテストコードを書くと割と大変なので、開発をしながらでもコツコツとテストコードを書くと良いです。

何をテストする?

Djangoのプロジェクトの構成を見てみましょう。

  • urls.py
  • views.py
  • models.py
  • forms.py

簡単に言うと、これらのお決まりファイルがエラーを出さず正常に動けば良いわけです。

なのでテストの対象はこれらのファイルの中身ということになります。

役割を知る

各ファイルの役割を知れば、どうテストすれば良いか明確になります。

  • urls.py
    pathからviewを呼び出す役割
  • views.py
    関数やクラスでレンダリング等を行う役割
  • models.py
    テーブルを作成する役割(フィールド名など)
  • forms.py
    フォームの設定やバリデーションをする役割

テストはこれらの役割が果たせているかどうかをチェックします。

テストの準備

デフォルトでは、アプリケーション直下にtests.pyが自動で作成されます。

この中に書いても良いのですが、コードが多いアプリケーションの場合、新たにtestsディレクトリを作成し、その中にテストファイルを作成すると管理しやすいです。

自動で作成されたtests.pyは削除します。

:file_folder: app名
  :file_folder: templates
    :file_folder: app名
      :page_facing_up: base.html
      :page_facing_up: index.html
      :page_facing_up: dashboard.html
  :file_folder: tests(作成)
    :page_facing_up: __ init __.py(作成)
    :page_facing_up: test_urls.py(作成)
    :page_facing_up: test_views.py(作成)
    :page_facing_up: test_models.py(作成)

test_が付いているファイルをDjangoが自動でテストしてくれます。

先頭にtest_がついていればファイル名は何でもOKです。

早速各ファイルが役割を果たせているかどうかチェック(テスト)してみましょう!

ここからは自身のテストコードを書きながら進めると体感できて良いです。

urls.pyのテスト

アプリ作成済みということ前提で、まずは簡単なurls.pyのテストからやってみようと思います。

urls.pyの役割はviewを呼び出すことなので、対象のviewが呼び出せているかチェックします。

urls.py
urlpatterns = [
    path('', views.index, name='index'),
    path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
]

このようなurls.pyがあった場合

exampe.com/で、indexの関数

exampe.com/dashboard/で、DashboardViewのクラス

が呼び出せていれば良いわけです。

test_urls.py
# TestCaseを読み込む(お決まり)
from django.test import TestCase
# reverseでnameからpathを、resolveでviewを呼び出す
from django.urls import reverse, resolve
# 全てテストする予定なので*でviewを全て呼び出す。
from .views import *

class TestsUrls(TestCase):
    """ indexの関数が呼び出せているかテスト """
    def test_index_url(self):
        url = reverse('appname:index')
        # url.funcが、indexと等しいかどうか
        self.assertEqual(resolve(url).func, index)

    """ DashboardViewのクラスが呼び出せているかテスト """
    def test_dashboard_url(self):
        url = reverse('appname:dashboard')
        # url.func.view_classが、DashboardViewと等しいかどうか
        self.assertEqual(resolve(url).func.view_class, DashboardView)
  • (TestCase)はお決まり
  • メソッド名は「test_」から始める
  • assertEqualは、第一引数と第二引数の値が等しい場合に成功します。第二引数に期待する値を書きます。
  • 関数(def)の場合、.func
  • クラス(class)の場合、.func.view_class
メソッド 内容 説明
assertEqual(a, b) a == b aとbが等しい時
assertNotEqual(a, b) a != b aとbが等しくない時

一旦テストしてみましょう!

terminal
$ python manage.py test

Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.01s

OK

OKがでれば成功です!

失敗した場合は以下の様な表示になり、エラーの内容とFAILED (errors=1)などが表示されるのでそこを修正しまします。

terminal
E.
======================================================================
ERROR: test_index_url (tests.tests.TestsUrls)
----------------------------------------------------------------------
エラーの内容
FAILED (errors=1)

views.pyのテスト

viewの役割は、関数やクラスでレンダリングや様々な指示を出すことなので、ここでは正常に画面が表示されるかをチェックします。

このようなviewがあるとします。

views.py
# ログイン必須
@login_required
def index(request):
    context = {}
    return render(request, 'appname/index', context)

# ログイン必須
class DashboardView(LoginRequiredMixin, ListView):
    model = User
    template_name = 'appname/dashboard.html'
    context_object_name = "users_list"
  • ログインしていない場合はログインページ+ログイン後のリダイレクト先が期待値
  • ログインしている場合のステータスコードは200が期待値
test_views.py
from django.test import TestCase
from .views import *

# ログインしていない場合
class TestsStatusCord(TestCase):
    ```トップページが/?next=付きログインページにリダイレクトされ遷移先で200を返すかテスト```
    def test_index_status_code(self):
        response = self.client.get('/')
        self.assertRedirects(response, '/accounts/login/?next=/')

    ```ダッシュボードが/?next=付きでログインページにリダイレクトされ遷移先で200を返すかテスト```
    def test_dashboard_status_code(self):
        response = self.client.get('/dashboard/')
        self.assertRedirects(response, '/accounts/login/?next=/dashboard/')

# ログインしている場合
class TestsLoginStatusCord(TestCase):
    ``` setUpで様々な共通の設定ができる ```
    def setUp(self):
        # ユーザーを作成
        user = User.objects.create(username='tarou', password='12345678', email='1@gmail.com')
        # ログインさせる
        self.client.force_login(user)

    ```トップページが正常に表示できるかテスト```
    def test_index_login_status_code(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)

    ```ダッシュボードが正常に表示できるかテスト```
    def test_dashboard_login_status_code(self):
        response = self.client.get('/dashboard/')
        self.assertEqual(response.status_code, 200)
terminal
$ python manage.py test

Found 6 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 0.12s

OK

テストで作成したオブジェクトは、テスト終了時に自動で削除されます。

Client()について

擬似的なブラウザでのアクセスを実行しResponseを取得することができるテスト用モジュールです。

# Clientをインスタンス化
client = Client()
client.get()

# 上と同等
self.client.get()

setUp()について

setUpは全メソッドで毎回実行されるので、共通のコードを書きます。

以下の内容と上記は同等です。

毎回メソッドでユーザー作成をすると重複するので、setUpに書いちゃった方が良いです。

test_views.py
class TestsLoginStatusCord(TestCase):
    ```トップページが正常に表示できるかテスト```
    def test_index_login_status_code(self):
+       user = User.objects.create(username='tarou', password='12345678', email='1@gmail.com')
+       self.client.force_login(user)
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)

    ```ダッシュボードが正常に表示できるかテスト```
    def test_dashboard_login_status_code(self):
+       user = User.objects.create(username='tarou', password='12345678', email='1@gmail.com')
+       self.client.force_login(user)
        response = self.client.get('/dashboard/')
        self.assertEqual(response.status_code, 200)

setUpTestData()について

setUpがメソッド毎に一度実行されるのに対して、setUpTestDataはクラス毎に一度実行されます。

当然、処理数が少なくなるのでserUpより早くテストが完了しますが注意が必要です。

setUpTestDataは、最初に一度だけ実行されるので、削除や更新が必要なメソッド(view)がある場合は、オブジェクトの状態が変わります。

test_
class TestsLoginStatusCord(TestCase):
    @classmethod
    def setUpTestData(self):
        self.user = User.objects.create(username='tarou', password='12345678', email='1@gmail.com')
        self.client.force_login(self.user)

    def test1(self):
        # self.userに対するテスト

    def test2(self):
        # self.userに対する他のテスト

models.pyのテスト

モデルはデータベースにテーブルを作成するものなので、フィールドから期待した値が取得できるかチェックしてみます。

色々と省略していますが、以下のユーザーmodelがあるとします。

models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser

class User(AbstractBaseUser):
    username = models.CharField(max_length=50, unique=True)
    email = models.EmailField(max_length=100, unique=True)
    # 日時
    date_joined = models.DateTimeField(auto_now_add=True)
    # 権限
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_admin = models.BooleanField(default=False)
test_models.py
from django.test import TestCase
from unittest import mock
from datetime import datetime

from .models import User

class UserModelTest(TestCase):
    ''' 初期値をテスト '''
    def test_default_values(self):
        # ユーザーを作成
        mock_date = datetime(2022, 10, 2, 20, 1, 11, 703055)
        with mock.patch('django.utils.timezone.now') as mock_now:
            mock_now.return_value = mock_date
            user = User.objects.create(username='tarou', password='12345678', email='1@gmail.com')

        self.assertEquals(user.username, 'tarou')
        self.assertEquals(user.email, '1@gmail.com')
        self.assertEquals(user.date_joined, mock_date)
        self.assertEquals(user.is_active, True)
        self.assertEquals(user.is_staff, False)
        self.assertEquals(user.is_admin, False)
        self.assertEquals(user.date_joined.strftime("%Y-%m-%d"), '2022-10-02')

mock_dateの部分について少し解説。

ユーザーを作成した時、date_joinedのフィールドがauto_now_add=Trueになっているので、現在の時刻が反映されてしまいます。

これだと第二引数の期待値を設定できないので、mockで仮の時刻を与えてあげます。

mock_dateで仮の日時を設定後、with内でtimezone.nowmock_dateを代入しユーザーを作成することで、date_joinedのフィールドの日時が、mock_dateの日時で作られます。

forms.pyのテスト

フォームについては、バリデーションがメインだと思うのでバリデーションのチェックをテストします。

test_forms.py
from Django.test import TestCase
from myapp.forms import MyForm

class TestForms(TestCase):

    def test_myform_test(self):
        form_data = {
            'username': 'tarou', 'email': '1@gmail.com'
        }
        form = MyForm(form_data)
        self.assertTrue(form.is_valid())

まとめ?

日時のテストは少し難しく感じたかもしれませんが、最初は簡単なところから一つだけテストコードを書いてみることをオススメします!

self.assertEqualsの形で書いているうちに、色々と理解できてくると思います。

アサートの種類についてはこちらの公式ドキュメントで確認できます。

記事に間違い等があればコメントくらさい(゚∀゚)

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9