Django Rest Framework + django.test.RequestFactoryを使うと期待値と異なるbodyが渡される

まとめ

Django Rest Frameworkを使っている場合、テストを書くときのRequestFactoryは
rest_framework.test.APIRequestFactory
を使ったほうがいい

起きたこと

テストとpostmanからリクエストを受けたときでRequestオブジェクト内のbodyの型が違ったので調べてみたら、
from django.test import RequestFactory が期待した形式ではないRequestオブジェクトを渡していた

... 前提 ...
使用パッケージ

Django Rest FrameworkでRest APIを作るとする。
ざっくりこんなクラスを用意して、ここにpostを投げるテストを書く。

class SampleViewClass(APIView):
    def post(self, request):
        title = request.data.get('param1')
        items = request.data.get('param2')

Django+RestFrameworkを使っている場合、RequestFactoryの選択肢が2つある。

  1. from rest_framework.test import APIRequestFactory
  2. from django.test import RequestFactory

こちらは from django.test import RequestFactory を使った場合のテストコードである。

from django.test import RequestFactory

class TestSampleViewClass(TestCase):

    def test_param(self):
        factory = RequestFactory()

        body = {'param1': 'nanakusa',
                'param2': ['seri', 'nazuna', 'gogyou', 'hakobera', 'other']
        }

        request = factory.post('/view', body, format='json')

        SampleViewClass.as_view()(request)

上記のRequestFactoryを経由してlistを含むjsonを渡した場合に問題が起きる。
SampleViewClass は以下のように値を受け取る。

class SampleViewClass(APIView):
    def post(self, request):
        title = request.data.get('param1')  # 'nanakusa'
        items = request.data.get('param2')  # 'other'

items にはなぜかstring型の 'other' が入る。

原因は RequestFactory がRequest.Bodyを QueryDict 型で保持しているため。
QueryDictはListをiteratorとして保持しているため、単純にgetするとlistで取得できず、list内要素の1つだけが返されてしまう。

一方、変数 title はStringがそのまま取得できていることからわかるように、この現象はiterate処理ができる型のbodyを受け取る場合のみに起きる。
StringやIntで構成されるようなリクエストの場合はどちらを使ってもさほど問題にはならないが、
階層構造があるリクエストを処理する場合に問題になる。

というわけで、Django Rest FrameworkでAPIを作っているときは、テストで使うRequestFactoryメソッドも APIRequestFactory で合わせたほうが安全。

参照

QueryDictタイプの挙動差異についてはこちらがわかりやすいので参照のこと。

qiita.com