最近追加されたテスティングフレームワークを試したみた。
Djangoは基本的にはPython由来のdoctestやunittestを用いてテストを行う。最近追加されたテスティングフレームワークは、アプリケーションの直下にあるmodels.pyやtests.pyを自動的にAllテストしてくれるというもの+Viewのテストを行う疑似ブラウザともいえるClientというクラス。
Railsと同じく、Djangoもテストの前にテスト用データベース・テーブルを生成し、初期データを流し込み(fixtureは現在実装中)、テストを行い、テスト用データベースを破棄するという流れ。おいおい、そんな流れは業務系とか既存データベース使うアプリにはできんぞ、せめてビューとかシノニムとかに気づいてくれよー。
とか思いつつ。(現実的には、デフォルトのTEST_RUNNER設定であるdjango.test.simple.run_testsをそのままは利用せずに、ちょっと振る舞いをかえたTEST_RUNNERを利用することになるんだろうな。Djangoは素直にコードが書いてあるから自作も簡単だろう。)
doctest
きっとモデルやマネージャに対して有効な気がするテスト記述方式。つか、面白い。
$ manage.py test --settings=djengel.custom_settings
とすると、テストが実施される。
このテストは、費目クラスの削除や表示フラグオフに対するテスト。削除も表示フラグオフも同じ動作になる。削除してもデータが消されないという肝心のテストが無いな・・。
class ExpenseItemManager(models.Manager) :
"""費目モデルのマネージャ。デフォルトのQuerysetに、非表示は取得しない、という条件を付与している。
"""
def get_query_set(self) :
return super(ExpenseItemManager,
self).get_query_set().filter(visible=True)
class ExpenseItem(models.Model) :
"""費目を表すモデル。
デフォルト動作の変更をいくつか実装してある。
- デフォルトのdeleteを上書き
- デフォルトのマネージャの上書き
- カスタムマネージャの追加。
>>> invisItem = ExpenseItem.objects.create(name="Test
Invisible", max_amount=4000, food_cost=False, visible=True)
>>> delItem = ExpenseItem.objects.create(name="Test Delete", max_amount=4000, food_cost=False, visible=True)
>>> before = ExpenseItem.objects.all()
>>> invisItem in before
True
>>> delItem in before
True
>>> invisItem.visible=False
>>> invisItem.save()
>>> delItem.delete()
>>> after = ExpenseItem.objects.all()
>>> invisItem in after
False
>>> delItem in after
False
"""
name = models.CharField(_('Expense Item Name'), blank=False,
maxlength=30)
max_amount = models.IntegerField(_('Max Amount'),
blank=False, default=0, help_text=_('Max amount per month.'))
food_cost = models.BooleanField(_('Food Cost Flag'),
default=False, help_text=_('weather food cost or not'))
visible = models.BooleanField(_('Visible Flag'),
default=True, help_text=_('if expense item is invisible, you can\'t
edit anymore.'))
objects = ExpenseItemManager() #デフォルトのマネージャをカスタムマネージャに変更
summary_objects = ExpenseItemSummaryManager() #サマリー用のマネージャを登録
class Admin :
list_display = ("name", "max_amount", "visible",
"food_cost")
ordering = ['id']
class Meta :
verbose_name = _('Expense Item')
verbose_name_plural = _('Expense Items')
def __str__(self) :
"""オブジェクトの人間可読表現
"""
return self.name
def delete(self) :
"""費目を物理削除すると関連する出費も含めて削除されてしまうため、モデルを利用しての削除はできないようにしてある。 代わりに、削除を呼ぶと「表示フラグ」をオフにするようにし、同時にマネージャをカスタムマネージャとし、デフォルトのQuerysetに「表示フラグ」オンという条件を入れるようにした。これにより、費目を管理画面から削除すると、「削除した費目を管理画面で編集できない」「削除した費目は出費の追加画面に候補として出てこない」という動作を実現している。 """
self.visible = False
self.save()
unittest
同様のテストをunittestを使って記述するとこうなる。まぁ、普通。testメソッドで例外が発生してもきちんとtearDownが走る。
import unittest
from djengel.squander.models import ExpenseItem
class ExpenseItemTest(unittest.TestCase) :
def setUp(self) :
self.invisItem =
ExpenseItem.objects.create(name="Test Invisible",
max_amount=4000, food_cost=False, visible=True)
self.delItem =
ExpenseItem.objects.create(name="Test Delete" ,
max_amount=4000, food_cost=False, visible=True)
def tearDown(self) :
pass
def test_normal(self) :
before = ExpenseItem.objects.all()
assert self.invisItem in before
assert self.delItem in before
def test_delete(self) :
self.delItem.delete()
after = ExpenseItem.objects.all()
assert self.delItem not in after
def test_invisible(self) :
self.invisItem.visible=False
self.invisItem.save()
after = ExpenseItem.objects.all()
assert self.invisItem not in after
Viewのテスト
普段業務で使っているJavaでは、特定のリクエストをした際に、HttpServletRequestに何が保存されているかを確認している。FlexからたたくJavaの場合は単にリターンをチェックしている。
Viewテストで行いたいのは、この程度。可能か?
結論から言えば、余裕でまかなえる。以下のテストのtest_loginを見てもらうと、responseからcontextを取り出すためにおかしなコードを挟んでいることに気づくと思う。
このテストでリクエストを出しているpathは、権限チェックが含まれるviewなので、ログインしていない状態でリクエストを出すと
のでresponseが3回疑似ブラウザに渡されている。
response.contextはこの3回の結果がリストとなって格納されている。contextにアクセスできるので、Viewで行った処理が正常かどうかを追うことができる(しかもリダイレクトされることまで追うことができる)。
ちなみに、Viewのテストにテストサーバの起動は不要。WSGIを使って疑似的(実は疑似ではない?)にリクエストを処理している。かなりたくさんの情報が取り出せる(responseの内容を表示してみれば何が入っているのかすぐわかる)ので、十分テスト可能。
import unittest
from djengel.squander.models import ExpenseItem
from django.test.client import Client
from django.contrib.auth.models import User, Permission
class SquanderViewTest(unittest.TestCase) :
def setUp(self) :
self.client = Client()
self.user = User.objects.create_user('mam', '',
'mampass')
view_permission =
Permission.objects.get(codename__exact='can_view')
self.user.user_permissions = [view_permission]
def tearDown(self) :
self.user.delete()
def test_login(self) :
respon =
self.client.login('/djengel/summary/2006/08/','mam', 'mampass')
last_response = respon.context[len(respon.context) -
1]
assert '2006/07/' == last_response.get('prev_month',
False)
print last_response.__dict__
def test_login_failed(self) :
assert not
self.client.login('/djengel/summary/2006/08/','dad', 'mampass')