DISTRICT 37

なにか

Djangoのテスト

テストの書き方としてModelのテスト、FORMのテスト、VIEWのテストをやったらいいのかなと。Djangoのテストフレームワークpythonのunittestに準じているので単体テストとなる。という事でバグを出さないことはもちろんのこと後述するCoverageを100%に近づけるのが目標となる。

という事で以降に自分がテストを書くときの方針をメモしていく

Modelのテスト

Modelのテストに関しては、フレームワークによりCRUDはできているものと考えていい。これがアレなようであればプルリクを投げつけよう。という事でテストすべきはModelに対して自分で書いた関数に対してとなる。オーバーライドしたstr関数とかをテストするといい。

class MyModelTest(TestCase):
    def setUp(self):
        self.model = MyModel.object.create(name="hoge", number=10)

    def test_str(self):
        assertEqual(str(self.model), "My name is hoge" )

setUp関数はテストをする前に呼び出される関数。あらかじめ(テストが不要な)処理をさせておく事でその他のテストで使い勝手がよくなる場合がある。

FORMのテスト

Formは画面の項目が入ってくる為、どういった入力条件がvalidとなり、どういった条件がinvalidになるかどうかをテスト。自作のclean関数とかをテストしたらいい。

class MyFormTest(TestCase):
    def test_valid(self):
        params = dict(
            name="hoge",
            number=10
        )
        myform = MyForm(params)
        self.assertTrue(myform.is_valid())


    def test_invalid(self):
        params = dict(
            name="hoge"
        )
        myform = MyForm(params)
        self.assertFalse(myform.is_valid())

formsetのテストの方法は少しめんどくさい。フレームワークがManagementFormを必要としているからだ。詳しくは公式を見たらいい

フォームセット (Formsets) | Django documentation | Django

class MyFormSetTest(TestCase):
    def test_valid(self):
        params = {
            "form-TOTAL_FORMS": 3,
            "form-INITIAL_FORMS": 1,
            "form-MAX_NUM_FORMS": '',
            "form-0-name": "name0",
            "form-0-number": 10,
            "form-1-name": "name1",
            "form-1-number": 20,
            }
        myform = MyFormSet(params)
        self.assertTrue(myform.is_valid())

VIEWのテスト

VIEWのテストがとてもめんどくさい。だが大事なテストである事には変わりない。ModelでもFormでもとりあえず方針は明確にはなってきているが、VIEWに対しては複合的にテストすることもある。基本的にはGET/POSTしたときの遷移とかFormの状態とか、処理後のモデルの状態とかを確認するようにしている。

TestCaseを継承したクラスはself.clientとしてあらかじめ定義されているので、Clientサイドのテストを行う事ができる

class MyListViewTest(TestCase):

    def setUp(self):
        self.model = MyModel.objects.create(name="hoge", number=10)

    def test_get(self):
        url = reverse("my_view")
        res = self.client.get(url)
        self.assertEqual(res.stats_code, 200)
        self.assertIEqual(len(res.context['models']), 1)

class MyDetailViewTest(TestCase):

    def test_get(self):
        url = reverse("my_detail_view", args=(self.model.pk, )) # /app/<int:pk>とかの引数として渡す
        res = self.client.get(url)
        self.assertEqual(res.stats_code, 200)

@login_requiredが必要な関数のテストはあらかじめログインの処理をしておく必要がある。

エラーになる事を(特にErrorをRaiseするといった)テストする場合にはassertの書き方が少し変わる。

class MyUpdateView(TestCase):

    def setUp(self):
        self.model = MyModel.objects.create(name="hoge", number=10)

    # 共通ログイン処理
    def login(self):
        User.objects.create_user('tmp', 'a@b.com', 'tmppass')
        login = self.client.login(username="tmp", password="tmppass")
        self.assertTrue(login)

    def test_get_with_login(self):
        self.login()
        url = reverse("my_update", args=(self.model.pk,))
        res = self.client.get(url)
        self.assertEqual(res.stats_code, 200)


    def test_get_without_login(self):
        url = reverse("my_update", args=(self.model.pk,))
        res = self.client.get(url)
        self.assertEqual(res.stats_code, 302) # ログイン画面とか別のページにリダイレクトされている事をテスト

    def test_post(self):
        self.login()
        url = reverse("my_update", args=(self.model.pk, ))
        data = {
            "name": "hogehoge",
            "number": 20
        }
        res = self.client.post(url, data)
        self.assertEqual(res.stats_code, 200)
        updatemodel = MyModel.objects.get(pk=self.model.pk)
        assertEqual(updatemode.name, "hogehoge")
        assertEqual(updatemode.number, 20)


    def test_post_with_error(self):
        self.login()
        url = reverse("my_update", args=(self.model.pk, ))
        data = {
            "name": "hogehoge",
            "number": 999
        }
        with assertRaises(ValueError):
            res = self.client.post(url, data)

テストの実行

実行はmanage.pyから実行する

python manage.py test myapp

これで一連のテストが実行できる。

特定のテストだけを行いたい場合は明記する

python manage.py test myapp.tests.TestMyClass

たくさんテストを書くとテストに時間が掛かることがある。少しでも時間を短縮させるためにテストをパラレルで実行することがオプションの追記できる。CPUの物理的な制限により期待した通り動かない場合もあるが、20%から30%ほどは時間が短縮できると実感できる。

python manage.py test --pararell 2

分割

テストスクリプトをたくさん書くことはいいことだ。だが、一つのファイルに書き込み続けると行数ばかりが増えていくばかりで今度は管理が難しくなってくる。公式にもあるとおり命名規則に従ってファイルを分割することで管理ができる。

どこにテストを書くべき?

デフォルトの startapp テンプレートは、新しいアプリケーション内に
tests.py ファイルを作成します。テストの数が少ないうちは、ここに書くのがいいかもしれませ
ん。しかし、テストスイートが大きくなってきたら、テストを複数のパッケージに再構成し
て、test_models.py 、 test_views.py 、 test_forms.py などの異なるサブモジュールに分離すると
良いでしょう。ファイル名には、ちゃんと組織的な命名規則になっていれば、自由に好きな名
前をつけて構いません。

Using the Django test runner to test reusable applications も参照してください。

Coverage

Coverageについては別記事にすることにした

dragstar.hatenablog.com