DjangoのViewをテストする

2006/09/03 23:46

※ 商品のリンクをクリックして何かを購入すると私に少額の報酬が入ることがあります【広告表示】

最近追加されたテスティングフレームワークを試したみた。

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')
2006/09/04 07:31 by ymasuda
#3711 対応済みです :)
2006/09/04 09:27 by nakagami
テスト入りのソースアーカイブはどっかに公開されないもんでしょうか?
2006/09/04 11:08 by everes
nakagamiさんにご指摘いただいた初期SQLの問題とf3cさんのMySQLパッチをいい感じに適用して、ほんの少しのテストコードを含ませたものを、近いうちに作成します。
が、ちょっと本業が忙しく(つらいスキーマのOracleにDjangoを突っ込んでいます)、週末待ちかもしれません。
> ymasudaさん
ひじょーに!助かります。

Prev Entry

Next Entry