mizzsugar’s blog

日々感じていることや学んだことを書きます。エンジニアリング以外にも書くかもしれません。

【Django】JsonResponseでのテストの仕方(GET/POST)

久々の投稿となってしまいました(^^;

今回使用したバージョン

* Python 3.7.1
* Django 2.1.7


今回サンプルとしてテストしたいメソッドはこちらのView関数

views.py

import json

from django.views.decorators.http import require_POST, require_GET
from django.http import JsonResponse

from .models import Item


@require_GET
def get_item(request, id: int) -> JsonResponse:
    """商品情報を取得するAPIです。

    :param request:
    :param id: ItemモデルのPK
    :return: JsonResponse
    """

    try:
        item = Item.objects.get(pk=id)
    except Item.DoesNotExist:
        return JsonResponse(
            data={},
            status=404
        )
    data = {
        'name': item.name,
        'price': item.price,
        'type': item.get_type_display()
    }
    return JsonResponse(
        data=data
    )


@require_POST
def create_item(request) -> JsonResponse:
    """商品情報を登録するAPIです。

    :param request:
    :return:
    """
    request_data = json.loads(request.body)
    Item.objects.create(
        name=request_data.get('name'),
        price=request_data.get('price'),
        type=request_data.get('type'),
        other_type=request_data.get('other_type')
    )

    data = {
        'message': '商品を登録しました'
    }
    return JsonResponse(
        data=data
    )


付随する情報たち

urls.py

from django.urls import path

from . import views

app_name = 'items'

urlpatterns = [
    path('<int:id>', views.get_item, name='get'),
    path('create', views.create_item, name='create'),
]

models.py

from django.db import models

from django.core.validators import MinValueValidator


class Item(models.Model):
    """商品モデル

    """

    # 品種
    TYPE_CHOICES = (
        (0, 'コーヒー'),
        (1, '紅茶'),
        (2, 'ココア'),
        (3, 'タピオカ'),
        (4, 'その他')
    )
    name = models.CharField(max_length=225)
    price = models.IntegerField(validators=[MinValueValidator(0)])
    type = models.SmallIntegerField(choices=TYPE_CHOICES)
    other_type = models.CharField(max_length=225, blank=True, null=True)  # typeがその他の場合

    class Meta:
        db_table = 'items'


テストしたい事項としては下記

get_item

  • パラメータで指定したIDの商品情報が返される

  • 存在しないIDがパラメータになったら404エラーとなる

  • 想定しないメソッドでリクエストが送られたら405エラーとなる

create_item

  • リクエストで送った内容の商品が登録される

  • 想定しないメソッドでリクエストが送られたら405エラーとなる


まず、get_itemのテストを。

商品情報が返されるか確認します。

JsonResponseの中身(dataの部分)が正しいか確認したいということで探していると dataの部分はresponse.contentとなっていることがわかりました。

>>> from django.http import JsonResponse
>>> response = JsonResponse({'foo': 'bar'})
>>> response.content
b'{"foo": "bar"}'

docs.djangoproject.com

しかし、contentはbytes型なのでそのままではテストできません。

調べたら、json.loads()によってcontentをJSON化して内容をテストしている事例がありました。

medium.com

結果、get_itemによって返される内容を確認するテストは下記のようになりました。

tests.py

import json

from django.test import TestCase, Client
from django.urls import reverse

from .models import Item


class GetItem(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.item = Item.objects.create(
            name='ゲイシャ',
            price=500,
            type=0
        )
        cls.client = Client()

    def test_get_item(self):
        response = self.client.get(
            path=reverse('items:get', kwargs={'id': 1})
        )

  # response.contentをJSON化する
        content = json.loads(response.content)
        with self.subTest('商品名が返される'):
            self.assertEqual(
                'ゲイシャ',
                content['name']
            )

        with self.subTest('値段が返される'):
            self.assertEqual(
                500,
                content['price']
            )

        with self.subTest('品種が返される'):
            self.assertEqual(
                'コーヒー',
                content['type']
            )


また、下記でもよかったようです。

content = response.json()

docs.djangoproject.com


続きましては、get_itemの異常系のテストです。

意図したステータスコードが返されるか確認します。

django.test.Clientによって作成されたdjango.test.Responseオブジェクトはアトリビュートstatus_codeにステータスコードを保持します。

response.status_codeでそのレスポンスのステータスコードがわかります。

docs.djangoproject.com

    def test_not_existing_item(self):
        response = self.client.get(
            path=reverse('items:get', kwargs={'id': 2})
        )
        content = json.loads(response.content)

        with self.subTest('コンテントは空である'):
            self.assertFalse(
                content
            )

        with self.subTest('404エラーで返される'):
            self.assertEqual(
                404,
                response.status_code
            )

    def test_request_post(self):
        response = self.client.post(
            path=reverse('items:get', kwargs={'id': 2}),
            data={}
        )
        with self.subTest('405エラーで返される'):
            self.assertEqual(
                405,
                response.status_code
            )


続いてcreate_itemのテスト。

下記でいけるかな〜と思いましたが、だめでした(^^;

def test_create_item(self):
        response = self.client.post(
            path=reverse('items:create'),
            data={
                'name': 'ハワイコナ',
                'price': 800,
                'type': 0
            },
        )

どうやら、content_type="application/json"がないと正しいcontent_typeでリクエストが送られなかった模様。

stackoverflow.com

デフォルトのcontent_typeがMULTIPART_CONTENTなので、そりゃ何も指定しないでうまくいくはずがありませんでした。

docs.djangoproject.com

結果、create_itemのテストはこうなりました。

    def test_create_item(self):
        response = self.client.post(
            path=reverse('items:create'),
            data={
                'name': 'ハワイコナ',
                'price': 800,
                'type': 0
            },
            content_type='application/json'
        )
        content = json.loads(response.content)
        with self.subTest('商品が登録される'):
            self.assertTrue(
                Item.objects.filter(name='ハワイコナ', price=800, type=0).exists()
            )

        with self.subTest('登録完了のメッセージが返される'):
            self.assertEqual(
                '商品を登録しました',
                content['message']
            )

    def test_request_get(self):
        response = self.client.get(
            path=reverse('items:create')
        )
        with self.subTest('405エラーで返される'):
            self.assertEqual(
                405,
                response.status_code
            )

2つの関数のテストをまとめるとこうなります。

tests.py

import json

from django.test import TestCase, Client
from django.urls import reverse

from .models import Item


class GetItem(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.item = Item.objects.create(
            name='ゲイシャ',
            price=500,
            type=0
        )
        cls.client = Client()

    def test_get_item(self):
        response = self.client.get(
            path=reverse('items:get', kwargs={'id': 1})
        )
        content = json.loads(response.content)
        with self.subTest('商品名が返される'):
            self.assertEqual(
                'ゲイシャ',
                content['name']
            )

        with self.subTest('値段が返される'):
            self.assertEqual(
                500,
                content['price']
            )

        with self.subTest('品種が返される'):
            self.assertEqual(
                'コーヒー',
                content['type']
            )

    def test_not_existing_item(self):
        response = self.client.get(
            path=reverse('items:get', kwargs={'id': 2})
        )
        content = json.loads(response.content)

        with self.subTest('コンテントは空である'):
            self.assertFalse(
                content
            )

        with self.subTest('404エラーで返される'):
            self.assertEqual(
                404,
                response.status_code
            )

    def test_request_post(self):
        response = self.client.post(
            path=reverse('items:get', kwargs={'id': 2}),
            data={}
        )
        with self.subTest('405エラーで返される'):
            self.assertEqual(
                405,
                response.status_code
            )

    @classmethod
    def tearDownClass(cls):
        pass


class CreateItem(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.client = Client()

    def test_create_item(self):
        response = self.client.post(
            path=reverse('items:create'),
            data={
                'name': 'ハワイコナ',
                'price': 800,
                'type': 0
            },
            content_type='application/json'
        )
        content = json.loads(response.content)
        with self.subTest('商品が登録される'):
            self.assertTrue(
                Item.objects.filter(name='ハワイコナ', price=800, type=0).exists()
            )

        with self.subTest('登録完了のメッセージが返される'):
            self.assertEqual(
                '商品を登録しました',
                content['message']
            )

    def test_request_get(self):
        response = self.client.get(
            path=reverse('items:create')
        )
        with self.subTest('405エラーで返される'):
            self.assertEqual(
                405,
                response.status_code
            )

    @classmethod
    def tearDownClass(cls):
        pass


まあ、DRFでやれよという話ですが、そちらはおいおい出来たらと思います!