djangoテストのassertQuerySetEqualメソッドに関するメモ

今回のお題
-
djangoテストのassertQuerySetEqualメソッドを使うにあたって必要だと感じた知識をメモしておきます。

assertQuerySetEqualメソッドとは
-
djangoの`TestCase`クラスに実装されているテスト用のメソッド。

あるレスポンスに想定通りのquerysetが含まれているかどうかをテストするために用いられる。

```python:assertQuerySetEqualメソッドの基本構文
self.assertQuerySetEqual(response.context["キー名"], "querysetオブジェクト"[, ordered=True])
# querysetオブジェクト同士の順番を無視する場合はordered=falseを指定する。
```

querysetオブジェクトとは
-
読んで字の如く`queryset型`のオブジェクトのこと。

`queryset型`とはdjangoに用意されているオブジェクトタイプの一つであり、`filter`, `all`, `exclude`などのメソッドで取得された`リスト`には`queryset`というオブジェクトタイプが与えられる（単一のオブジェクトの場合にはqueryset型にはならない）。

```python:オブジェクトタイプの例
# モデルの例
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`の中身について見てみる。

```python:ListVeiwにアクセスした場合
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>}]]
```
色々とリクエストに関する情報が出てくるが、以下に注目する。

```python
'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`と`モデル名`になる。

```python:DetailViewの場合
print(response.context)
>>
# 中略
'object': <Shop: テスト店舗1>, 'shop': <Shop: テスト店舗1>
# 以下省略
```

DetaiViewなどであっても、`get_context_data`メソッドの中などで別途取得したオブジェクトに関してはリスト形式であれば`queryset型`になる。


```python
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`またはモデル名)

```python:assertQuerySetEqualパターン
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に対しても使用可能。
```

```python:単一オブジェクトパターン
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のキーの指定方法を押さえておけば基本的には大丈夫かなと思います。