diadia

興味があることをやってみる。自分のメモを残しておきます。

Djangoのテストを実装

分かったこと

  • djangoでテストする項目がわかった。
  • どんな感じでdjangoのテストを書くのか分かった。
  • djangoのテストはどんな種類か

分かったことを軽くまとめる

ふわっと理解する

テストは開発中のアプリが動くことでこうあって欲しい、こうあるべきという結果をまず設定する。 そしてその結果が生じる環境を準備し、その環境下で実際に開発しているアプリを起動させてその結果を得る。 そして最初に設定した結果と実際アプリが動いて得られた結果を照合する。 照らし合わせた結果として実際の結果が想定する結果になれば動作保証がそのテストにおいて保証される。 逆に両者の結果が異なれば、アプリ内のロジックに問題があるか、テスト環境作成に問題があるか、テスト自体に瑕疵があるかである。どれに該当するか考え、該当した箇所を改善し、再びテストを実行する。 djangoにおけるテストを実行するドキュメントは、テストフレームワークを利用した自動化テストに関するものである。 自動化テストとは、テストを起動するコマンドを入力するとこれまで、テストコードが走り、自動的にテストしたい環境を作成し、あるべき結果と現状のアプリが走った結果を照合し、そのテスト結果を記録する。すべてのテストが終了した後にすべてのテスト結果を出力し確認する。

自動化テストのメリットは開発中のアプリの品質を部分的に保証し開発者に安心感を与えること、以前記述したロジックに手を加えることで品質が劣化していないか確認することを手軽にチェックすることができる。

以前持っていた疑問とそれに対する答え

Q1.テストする項目は?

  1. endpointに対して呼び出されるメソッドが正しいか(urls.pyの検証)
  2. endpointに対して使われるテンプレート(html)が正しいか
  3. formに対して与えられるデータによってバリデーションが機能するか(form)
  4. POSTリクエストで投げたデータが正しく保存されているか(views.py)
  5. アクセスするユーザーの種類によってリダイレクトされたり、テンプレートが変わるか(views.py)
  6. 返されるレスポンスのcontextに必要十分のデータが格納されているか(views.py)

Q2. djangoのテストはどんな特徴? ブラックテストとホワイトテストをClientクラスやFactoryクラス?で使い分けることができる。 ただ大体リクエストを投げてレスポンスをテストの対象にすることが多かったのでホワイトテストでテストを書くことが多くなると思う。

基本的にpythonのunuttestを使って実装するイメージをもてばよい。

djangoのテストはほとんどホワイトテストに基づく。つまりdjangoアプリケーションの内部ロジックに基づいたテストを実装する。

Django.test.TestCaseのサブクラスを使ってテストパターンを作成していく。

Djangoにもテストがあるし、GeoDjangoにもテストがあるし、DRFにもテストがある。 

これらをまとめてみたい。

テスト内容とdjangoのモジュールの関係 ユニットテスト

views.pyはclientでテストを実行する。 models.pyもテスト実行する。けどmodelのメソッドに対して実行すると思われる。

test*.pyのファイルに入っているTestCaseを継承したクラスがテストに追加され、実行される。

テストの種類 djangoのunittest
特定の環境下でrequestを投げてどのテンプレートが返されるか、どんなオブジェクトがレンダリングされているかを確認した

seleniumを使ったフロントエンドのテスト
特定の環境下でリクエストを投げて表示されるhtmlのボタンやアンカータグのリンク先が適切なViewのnameが使われているか確認した

django rest frameworkのテスト
特定の環境下でリクエストを投げて返されるオブジェクトを確認した

DBの扱いについて

djangoのテストでは、テスト実行時にテスト用のDBが生成される。そしてテスト終了時にデフォルトでは作成されたDBが削除されることになる。 settings.pyで'TEST'を宣言する必要があると分かった。

DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        'NAME': 'postgis_test',
        'PORT': '5432',
        'USER': 'postgisuser',
        'PASSWORD': 'geodjango_passw0rd',
        'TEST':{"NAME" : "test_postgis_db"},
    }
}

テストデータの作成について

テストデータの作成には、実際にendpointを叩いてitemオブジェクトを生成する方法とTestCase#setUpTestDataメソッドを使う方法がある。

実際にエンドポイントを叩く方法は例えばあるメソッド内(setUpまたはあるテスト)でユーザーオブジェクトを生成するエンドポイントと生成するためのユーザーデータをpostする。
こうするとテスト実行時のオブジェクトが生成される。

その他の方法としてオブジェクトをORMを使って生成する方法もある。 例えばItem.objects.create(title="test", price=100)
こんな感じ生成してもテスト実行時のオブジェクト(テスト用のデータ)が生成される。

テストデータの作成はTestCase#setUpを使って生成することを覚えた。 現状としてはこのメソッドでオブジェクトを生成し、各テストでClientオブジェクトを使ってcontextの確認を行うというテストを行っている。

テストデータの作成(Userオブジェクトの作成)

Userオブジェクトの作成は認証ユーザーがrequestを送る場合のテストを検証するため重要になる。 ログインするための使えるUserオブジェクトの作り方は2種類存在する。

  • ユーザー登録を実行するエンドポイントを実装し、そこに作成するUserデータを送信して作成する
  • User.objects.create_user(username="hieh", password="12345")を実行する

誤ってもUser.objects.create()でUserオブジェクトを作成しないこと。作成したとしてもpasswordの再現が困難でありユーザー認証する事ができないから。

Djangoのテストをどう書くか?

例)メソッドに対して返されるHTMLが正しいかどうかを確かめるサンプル

class HogeTest(TestCase):

    url = "/hoge/idvvokb/"
    def setUp(self):
        User.objects.create_user(
            username="test_user",
            password="12345")


    def test_templates_by_anonymous_user(self):
        self.client = Client()
        login_status = self.client.logout()
        #未認証ユーザーでアクセス
        self.assertFalse(login_status) 


        response = self.client.get(self.url)
        templates = [ele.name for ele in response.templates]
        #hoge/template1.htmlが含まれない
        self.assertTrue("hoge/template1.html" not in templates) 
        #hoge/template2.htmlが含まれない
        self.assertTrue("hoge/template2.html" not in templates) 
        #hoge/template3.htmlが含まれない
        self.assertTrue("hoge/template3.html" not in templates) 
        
    def test_templates_by_authenticated_user(self):
        self.client = Client()
        login_status = self.client.login(username="test_user", password="12345")
        #認証ユーザーでアクセス
        self.assertTrue(login_status) 

        response = self.client.get(self.url)
        templates = [ele.name for ele in response.templates]
        #hoge/template1.htmlが含まれる
        self.assertTrue("hoge/template1.html" in templates)
        #hoge/template2.htmlが含まれる
        self.assertTrue("hoge/template2.html" in templates) 
        #hoge/template3.htmlが含まれる
        self.assertTrue("hoge/template3.html" in templates) 

TestCase#setUpメソッドで行うことはテストデータを作成することだと現状認識している。
setUpメソッドで生成したオブジェクトがdjangoに存在するすべてのオブジェクトになる。(シグナル等の何かをトリガーとして他のオブジェクトが生成される場合はそのオブジェクトもテスト対象のオブジェクトなる。)
Clientクラスはurlに基づいてviewsの各ビューを起動してレスポンスを得ることができる。
htmlファイルが適切に使われるかをチェックするためにはresponseにたいしてtemplatesを使い、その各要素に対してnameを使うと各htmlを参照することができる。

self.clientを各メソッドで初期化しているけれども、これはsetUpでself.clientを初期化しても各メソッドでself.clientの内容が引き継がれてしまうからである。

Formのテスト

送信するデータのバリデーションテスト

import HogeForm

data = {"title": "じじゃおdsv", "price":900 }
form = HogeForm(data)
self.assertTrue(form.is_valid())

レスポンスに使用されるformのクラスが一致しているかのテスト

self.client = Client()
login_status = self.client.login(username="access_user", password="12345")
self.assertTrue(login_status)
response = self.client.get(reverse_lazy('profiles:profile'), follow=True)
self.assertTrue(response.status_code, 200)
self.assertTrue(type(response.context["form"]), ProfileForm)

FormにForeignKeyが含まれる際に注意すること

djnago test ModelFormにForeignKeyが含まれる場合のテスト - diadia

seleniumを使ったテストを実装する

Seleniumを使ったテストのメモ - diadia

テンプレートのテスト

レンダリングされるテンプレートのテスト

django.test.Clientオブジェクトを使ったresponseにはtemplatesがある。 これを使うことでレスポンスで表示されるテンプレートを確認する事ができる。

def test_テンプレートのテスト(self):
    self.client = Client()
    response = self.client.get("/hige/hoge/")
    templates = [template.name for template in response.templates]
    self.assertTrue("hsi/ddjfdi.html" in templates)

またこのようにしなくてもassertTemplateUsedやassertTemplateNotUsedを使ってもテストは行える。

def test_テンプレートのテスト(self):
    self.client = Client()
    response = self.client.get("/hige/hoge/")
    self.assertTemplateUsed(response, template_name="hsi/ddjfdi.html" )
    self.assertTemplateNotUsed(response, template_name="hsi/dcds.html" )

レンダリングされるテンプレートに含まれる要素のテスト

レスポンスとして返されるテンプレートにはhtmlタグやタグの属性値またはタグのテキストが含まれているか確認したい場合は、
self.assert(Not)Contains(response, "文字列") で対応する事ができる。

self.client = Client()
login_status = self.client.login(username="test1", password="1234tweet")
self.assertTrue(login_status)
response = self.client.get(reverse_lazy(ViewName.ITEM_CREATE))
self.assertContains( response, 'action="/items/create2/"', status_code=200 )
self.assertNotContains( response, '/edit/', status_code=200 )

ForeignKeyのフィールドがnullかどうかの判断

if book.author is None:
    print("nullです")

Django Rest Frameworkのテストの作り方

django rest frameworkの場合もホワイトテストでテストを作成する。 その中で困ることがある。それはtokenを使った認可によるリソースのアクセスである。認証の場合には以下のようにすればよかった。

self.client = Client()
login_status = self.client.login(username="hhej", password="saahfa")

認可に関しては当然認証の方法(login)が使えない。したがってrest_frameworkのテストモジュールを利用する必要性がある。

具体的に使うのはAPIClientである。APIClientモジュールにはtokenをヘッダーにセットする機能を有するのでhttpリクエストを実行する前にtokenをセットして使用する。

from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

token = Token.objects.get(user__username='access_user')
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)

response = self.client.patch(url)

トークンによるリソースアクセスの方法を示した資料

django rest framework - Add Token to headers request in TEST mode DRF - Stack Overflow

DRFのResponseのデータを参照する方法

Responseの内容はresponse.dataで参照することができる。値はdict型である。

from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

token = Token.objects.get(user__username='access_user')
self.client = APIClient()
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)

response = self.client.patch(url)
print(response.data)

Django Test Kotlinのretrofitが送信するデータを再現する

retrofitで送信されるデータ形式djangoで再現する。 Django Test retrofitが送信するデータを再現する - diadia

その他注意点

urlは最初に/から始める。/から始めないと404のコードがかえってきてしまうので注意する。
アクセス時にセッションを追加したいときは以下の資料を参照のこと。
https://docs.djangoproject.com/en/dev/topics/testing/tools/#django.test.Client.session

参考資料

Django におけるテスト | Django ドキュメント | Django
Djangoのテストの書き方について勉強したのでまとめる - c-bata web