Python Djangoで単体テストをしてみました

Pythonでテストを書いたことがなかったので、

以前作ったDjangoのWebアプリの単体テストを作成してみました。



mtomitomi.hatenablog.com






参考



realpython.com






Djangoのテストファイルについて



アプリケーション作成時にtests.pyというファイルが自動で作成されているので、このファイルを編集しました。
test〜で始まるファイルかつ、test〜から始まるメソッドで書かないと、テストとして認識してくれないようです。

また、参考にしたドキュメントによると、ModelsやViewsごとにテストファイルを分けたほうが良いようなので、分けて作成しました。

共通で使うテスト用のデータは、init.pyに記載しておきました。




tests
├── __init__.py
├── test_forms.py
├── test_models.py
├── test_views.py
├── tests.py
└── tests_selenium.py





テストの実行はターミナルから下記コマンドで実行できますが、
私はPyCharmから実行して確認しました。




$ ./manage.py test





f:id:mtomitomi:20180204222705p:plain






Modelsのテスト



テスト用に作成したBookオブジェクトです。

init.py




from cms.models import Book


def create_book(self, name="はじめてのDjango", publisher="d jango", page="50"):
return Book.objects.create(name=name, publisher=publisher, page=page)







データが登録されていない確認



初期状態で書籍データが何も登録されていないことの確認です。

assertEqual(a, b)は「a == b」の確認をしています。




def test_default_models(self):
saved_book = Book.objects.all()
self.assertEquals(saved_book.count(), 0)





TestCaseで用意されているassetでの比較メソッドについては、下記のドキュメントを参照しました。



26.4. unittest — ユニットテストフレームワーク — Python 3.6.3 ドキュメント






1件データが登録されたことの確認



データベースにオブジェクトを保存するときはsave()を使います。
これはModelクラスに用意されているメソッドで、オーバーライドして使うことができます。




def test_one_models(self):
book = Book()
book.save()
saved_books = Book.objects.all()
self.assertEquals(saved_books.count(), 1)






Bookオブジェクトが期待通りに生成できたかの確認



Bookオブジェクトのインスタンス生成有無と、書籍名の確認をしています。




def test_book_create(self):
book = create_book(self)
self.assertTrue(isinstance(book, Book))
self.assertEquals(book.__str__(), book.name)






Viewsのテスト



レスポンスの確認



レスポンスのステータスコードが200(成功)かの確認と、レスポンスに書籍名が含まれているかの確認をしています。

reverse()はURLの取得を行なっています。URLの名前空間は2つに分かれていて、それぞれ{アプリケーション名:ページ名}を指定します。

ちなみに日本語(書籍名)をassertで比較しようとすると、
レスポンスで返ってきた値がバイト列になっていて結果NGになってしまうので、
bytes()を使って文字列に変換しています。




class ViewTest(TestCase):

def test_book_list_view(self):
book = create_book(self)
url = reverse('cms:book_list')
resp = self.client.get(url)

self.assertEquals(resp.status_code, 200)
self.assertIn(book.name, bytes(resp.content).decode('utf-8'))





補足ですが、reverse()で指定するときのページ名は、

urls.pyのname=に設定されている値です。




from django.conf.urls import url
from cms import views

urlpatterns = [
# 書籍一覧
url(r'^book/$', views.book_list, name='book_list'),






Formsのテスト



オブジェクトの生成確認



BookFormオブジェクトのデータの検証をするテストです。

正常な値を指定しているので、BookFormオブジェクトが生成されます。




def test_valid_form(self):
book = Book.objects.create(name='Django Book', publisher='d jan go', page='100')
data = {'name': book.name, 'publisher': book.publisher, 'page': book.page, }
form = BookForm(data=data)
self.assertTrue(form.is_valid())






オブジェクトが生成されない確認



無効なデータの確認をしています。

nameは必須の値なので、エラーになります。




def test_invalid_form(self):
book = Book.objects.create(name='', publisher='', page='0')
data = {'name': book.name, 'publisher': book.publisher, 'page': book.page, }
form = BookForm(data=data)
self.assertFalse(form.is_valid())






seleniumFirefoxを起動



参考にしたドキュメント通りにseleniumをインストールしてプログラムを実行すると、Firefoxが起動します。

最新版をインストールすると、他にもファイルをダウンロードする必要があるようでエラーになります。
今回はseleniumを使ったテストはせず、起動のみ確認しました。




$ pip install selenium==2.33.0







import unittest
from selenium import webdriver


class TestSignup(unittest.TestCase):

def setUp(self):
self.driver = webdriver.Firefox()

def test_signup_fire(self):
self.driver.get("http://localhost:8000/add/")
self.driver.find_element_by_id('id_title').send_keys("test title")
self.driver.find_element_by_id('id_body').send_keys("test body")
self.driver.find_element_by_id('submit').click()
self.assertIn("http://localhost:8000/", self.driver.current_url)

def tearDown(self):
self.driver.quit()


if __name__ == '__main__':
unittest.main()






f:id:mtomitomi:20180204222821p:plain






やってみた所感



Djangoはテストライブラリが付いているので便利でした。

Pythonでテストを書くのは初めてだったので、参考になる資料を探してみましたが、
色々な書き方があるようなのでどれを参考にするべきか非常に悩みました。

まだ、良い書き方が判断できないので、
テストする観点を明確にして最低限それを満たすテストを書き、
githubなど参照して良い書き方があれば参考にするようにしたいと思います。





足りていないテストケースもありますし、
seleniumでの自動実行や、カバレッジでの確認などもできるようなので、
後日試してみたいと思います。






【補足】テストコード全体



test_forms.py




from django.test import TestCase
from cms.forms import BookForm
from cms.models import Book


class ViewTest(TestCase):

def test_valid_form(self):
book = Book.objects.create(name='Django Book', publisher='d jan go', page='100')
data = {'name': book.name, 'publisher': book.publisher, 'page': book.page, }
form = BookForm(data=data)
self.assertTrue(form.is_valid())

def test_invalid_form(self):
book = Book.objects.create(name='', publisher='', page='0')
data = {'name': book.name, 'publisher': book.publisher, 'page': book.page, }
form = BookForm(data=data)
self.assertFalse(form.is_valid())





test_views.py




from django.core.urlresolvers import reverse
from django.test import TestCase
from cms.tests import create_book


class ViewTest(TestCase):

def test_book_list_view(self):
book = create_book(self)
url = reverse('cms:book_list')
resp = self.client.get(url)

self.assertEquals(resp.status_code, 200)
self.assertIn(book.name, bytes(resp.content).decode('utf-8'))





test_models.py




from django.test import TestCase
from cms.models import Book
from cms.tests import create_book


class ViewTest(TestCase):

def test_default_models(self):
saved_book = Book.objects.all()
self.assertEquals(saved_book.count(), 0)

def test_one_models(self):
book = Book()
book.save()
saved_books = Book.objects.all()
self.assertEquals(saved_books.count(), 1)

def test_book_create(self):
book = create_book(self)
self.assertTrue(isinstance(book, Book))
self.assertEquals(book.__str__(), book.name)






github.com