今回のお題
djangoテストのassertQuerySetEqualメソッドを使うにあたって必要だと感じた知識をメモしておきます。
assertQuerySetEqualメソッドとは
djangoのTestCase
クラスに実装されているテスト用のメソッド。
あるレスポンスに想定通りのquerysetが含まれているかどうかをテストするために用いられる。
self.assertQuerySetEqual(response.context["キー名"], "querysetオブジェクト"[, ordered=True])
# querysetオブジェクト同士の順番を無視する場合はordered=falseを指定する。
querysetオブジェクトとは
読んで字の如くqueryset型
のオブジェクトのこと。
queryset型
とはdjangoに用意されているオブジェクトタイプの一つであり、filter
, all
, exclude
などのメソッドで取得されたリスト
にはqueryset
というオブジェクトタイプが与えられる(単一のオブジェクトの場合にはqueryset型にはならない)。
# モデルの例
class Book(models.Model):
title = models.CharField(max_length=20)
def __str__(self):
return self.title
# インスタンス
book1 = Book.objects.create(title="坊ちゃん")
book2 = Book.objects.create(title="吾輩は猫である")
book3 = Book.objects.create(title="三四郎")
book_set = Book.objects.all()
# 単体のインスタンスは<モデル名: __str__メソッドの戻り値>
<Book: 坊ちゃん>
# リストの場合は<QuerySet: [<要素1>, <要素2>, ...]>
<QuerySet: [<Book: 坊ちゃん>, <Book: 吾輩は猫である>, <Book: 三四郎>]>
response.contextの中身
次に、assertQuerySetEqulメソッドの第一引数であるresponse.context
の中身について見てみる。
c = Client()
response = c.get(reverse("shop:list"))
print(response.context)
>> [[{'True': True, 'False': False, 'None': None}, {'csrf_token': <SimpleLazyObject: <function csrf.<locals>._get_val at 0x10802c310>>, 'request': <WSGIRequest: GET '/shop/list/'>, 'user': <SimpleLazyObject: <django.contrib.auth.models.AnonymousUser object at 0x10802e3d0>>, 'perms': <django.contrib.auth.context_processors.PermWrapper object at 0x10802e0a0>, 'messages': <django.contrib.messages.storage.fallback.FallbackStorage object at 0x108026a30>, 'DEFAULT_MESSAGE_LEVELS': {'DEBUG': 10, 'INFO': 20, 'SUCCESS': 25, 'WARNING': 30, 'ERROR': 40}}, {}, {'paginator': None, 'page_obj': None, 'is_paginated': False, 'object_list': <QuerySet [<Shop: テスト店舗>, <Shop: テスト店舗0>, <Shop: テスト店舗1>]>, 'shop_list': <QuerySet [<Shop: テスト店舗>, <Shop: テスト店舗0>, <Shop: テスト店舗1>]>, 'view': <shop.views.ShopListView object at 0x108026d00>}], [{'True': True, 'False': False, 'None': None}, {'csrf_token': <SimpleLazyObject: <function csrf.<locals>._get_val at 0x10802c310>>, 'request': <WSGIRequest: GET '/shop/list/'>, 'user': <SimpleLazyObject: <django.contrib.auth.models.AnonymousUser object at 0x10802e3d0>>, 'perms': <django.contrib.auth.context_processors.PermWrapper object at 0x10802e0a0>, 'messages': <django.contrib.messages.storage.fallback.FallbackStorage object at 0x108026a30>, 'DEFAULT_MESSAGE_LEVELS': {'DEBUG': 10, 'INFO': 20, 'SUCCESS': 25, 'WARNING': 30, 'ERROR': 40}}, {}, {'paginator': None, 'page_obj': None, 'is_paginated': False, 'object_list': <QuerySet [<Shop: テスト店舗>, <Shop: テスト店舗0>, <Shop: テスト店舗1>]>, 'shop_list': <QuerySet [<Shop: テスト店舗>, <Shop: テスト店舗0>, <Shop: テスト店舗1>]>, 'view': <shop.views.ShopListView object at 0x108026d00>}]]
色々とリクエストに関する情報が出てくるが、以下に注目する。
'object_list': <QuerySet [<Shop: テスト店舗>, <Shop: テスト店舗0>, <Shop: テスト店舗1>]>, 'shop_list': <QuerySet [<Shop: テスト店舗>, <Shop: テスト店舗0>, <Shop: テスト店舗1>]>
ListViewはmodel, queryset, get_querysetメソッドのいずれかでテンプレートに表示するオブジェクトを指定する。
このオブジェクトのリストはobject_list
(およびモデル名_list
)というキーでcontextの中に格納される(すなわち同一のquerysetが複数のキーに対応することになる。ただし、get_querysetメソッドの処理が複雑な場合にはmodel名_list
というキーが作られない場合もある)。
また、DetailViewのような単一のオブジェクトを返すビューではキー名はobject_list
, モデル名_list
ではなくobject
とモデル名
になる。
print(response.context)
>>
# 中略
'object': <Shop: テスト店舗1>, 'shop': <Shop: テスト店舗1>
# 以下省略
DetaiViewなどであっても、get_context_data
メソッドの中などで別途取得したオブジェクトに関してはリスト形式であればqueryset型
になる。
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["canceled_orders"] = self.object.order_set.dead()
return context
print(context)
>>
'canceled_orders': <LogicalDeletionQuerySet: []>
# LogicalDeletionMixinを継承しているオブジェクトに関してはLogicalDeletionQuerySet型になる。
まとめ
レスポンスにqueryset
が含まれる場合には、assertQuerySetEqual
メソッドを用いてテンプレートに渡されたオブジェクトを確認する。
単一のオブジェクトの場合にはassertQuerySetEqualメソッドは使えない(使う必要もない)ので、assertEqualメソッドを用いてオブジェクトが期待通りかを検証する(キーはobject
またはモデル名)
def test_list_view(self):
response = Client().get(reverse("Book:list"))
self.assetQuerySetEqual(response.context["object_list"], [<Book: 坊ちゃん>, <Book: 吾輩は猫である>, <Book: 三四郎>], ordered=False)
# contextのキーはbook_listでもOK
self.assetQuerySetEqual(response.context["book_list"], [<Book: 坊ちゃん>, <Book: 吾輩は猫である>, <Book: 三四郎>], ordered=False)
# modelやget_querysetメソッドではなくget_context_dataでセットしたquerysetについてはその時のキーを用いる
self.assetQuerySetEqual(response.context["canceled_orders"], [])
# assertQuerySetEqualメソッドは、LogicalDeleteQuerySetに対しても使用可能。
def test_detail_view(self):
book = Book.objects.first()
response = Client().get(reverse("Book:detail", kwargs={"pk": book.pk))
self.assertEqual(response.context["book"], book)
# 以下でもOK
self.assertEqual(response.context["object"], book)
QuerySetがリストのオブジェクトタイプの一つであるということ、単一のオブジェクトには使えないこと、そしてcontextのキーの指定方法を押さえておけば基本的には大丈夫かなと思います。