[Django REST Framework] APIClientのPatchでJSON ParseErrorが発生してしまう件について

## 概要
Django REST Frameworkでテスト中、`from rest_framework.test import APIClient`を使用したPatchメソッドで`JSON ParseError`が出てしまった場合の解決方法を紹介！

## 目的
掲題の問題の解決と解説

## 使用技術

```pyproject.toml
# pyproject.tomlの一部
python = "^3.11.2"
Django = "^4.2.1"
djangorestframework = "^3.14.0"
pytest = "^7.3.1"
pytest-django = "^4.5.2"
```
## APIClient().patch()がうまくいかない...

ユーザー情報を変更するAPIに対するテストで起こった出来事です

```test_sample.py
import pytest
from rest_framework.test import APIClient


@pytest.fixture
def client(scope="session"):
    """APIクライアントを作成"""
    return APIClient()

 
@pytest.fixture
def partial_update_url():
    """ユーザー情報変更APIのURL"""

    def _method(id):
        return f"/api/users/{id}"

    return _method


@pytest.fixture
def payload():
    return {"name": "sotaheavymetal21", "user_role": "偉い人"}

@pytest.mark.django_db
def test_user_partial_update(
    admin_user, general_user, partial_update_url, client, payload
): 
    # conftest.pyにフィクスチャを複数用意しています（説明は省略）
    """ユーザーの情報を変更できるか"""
    response = client.patch(
        partial_update_url(general_user.id),data=payload, format="json"
        )

    assert response.json() == {"detail": "ユーザー情報を変更しました"}

```

上記のテストを実行してみると、以下のエラーが返却されました

```
{'detail': 'リクエストのメディアタイプ "application/octet-stream" はサポートされていません。'} 
```
:::note warn
？！　
:::


### 私は驚きました
以前までたくさんテストを実装してきて、今回も同様にやったつもりでした
違うのはHTTPメソッドが違う点のみです（以前はGETやPOSTでのテストが全て）

### じゃあ、"application/json"を指定してあげよう
```test_sample.py
    response = client.patch(
        partial_update_url(general_user.user.id),
        data=payload,
        headers={"Content-Type": "application/json"},
    )
```


以下のエラーが出ました
```
{'detail': 'JSON parse error - Expecting property name enclosed in double quotes: line 1 column 2 (char 1)'} 
```
:::note warn
？！　
:::


パースエラー...
なぜ...


## エラーが起きた原因はこれ

https://stackoverflow.com/questions/39906956/patch-and-put-dont-work-as-expected-when-pytest-is-interacting-with-rest-framew


上記記事より引用↓
> Pytest uses the django test client and client.post default content_type is multipart/form-data, while put, patch and delete use application/octet-stream.

> That's why this is tricky sometimes. Even with post requests, if you plan to support JSON payload, you must tell the content type in the test request. Anyway, with recent Django versions, you can just pass the data object to the client request and it will be serialized for you, as declared in the docs:

> If you provide content_type as application/json, the data is serialized using json.dumps() if it’s a dict, list, or tuple. Serialization is performed with DjangoJSONEncoder by default, and can be overridden by providing a json_encoder argument to Client. This serialization also happens for put(), patch(), and delete() requests.


### 重要な点は以下
1. PytestがDjangoのテストクライアントを使用し、`client.post`のデフォルトのcontent_typeは`multipart/form-data`である一方、putやpatch、deleteは`application/octet-stream`を使用している

2. 実際、最新のDjangoのバージョンでは、データオブジェクトを単にクライアントリクエストに渡すことで、データが自動的にシリアライズされ、ドキュメントで宣言されている通りに動作することが可能

3. content_typeを`application/json`として提供する場合、データは辞書、リスト、またはタプルの場合に `json.dumps()`を使用してシリアル化される
3. デフォルトでは`DjangoJSONEncoder`によってシリアル化が実行される
3. このシリアル化は、put()、patch()、および delete() リクエストに対しても同様に行われる


## APICientのコード

```django-rest-framework/rest_framework/test.py
class APIClient(APIRequestFactory, DjangoClient):
    def __init__(self, enforce_csrf_checks=False, **defaults):
        super().__init__(**defaults)
        self.handler = ForceAuthClientHandler(enforce_csrf_checks)
        self._credentials = {}


       # ~~この間省略~~ 

       
    def post(self, path, data=None, format=None, content_type=None,
             follow=False, **extra):
        response = super().post(
            path, data=data, format=format, content_type=content_type, **extra)
        if follow:
            response = self._handle_redirects(response, data=data, format=format, content_type=content_type, **extra)
        return response


    def patch(self, path, data=None, format=None, content_type=None,
              follow=False, **extra):
        response = super().patch(
            path, data=data, format=format, content_type=content_type, **extra)
        if follow:
            response = self._handle_redirects(response, data=data, format=format, content_type=content_type, **extra)
        return response
```


## 結論、こう改善しよう！

```test_sample.py
    response = client.patch(
        partial_update_url(general_user.id),
        data=payload,
        content_type="application/json",
    )
```


## 参考　
- https://stackoverflow.com/questions/39906956/patch-and-put-dont-work-as-expected-when-pytest-is-interacting-with-rest-framew
- https://www.django-rest-framework.org/api-guide/testing/#making-requests
- https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types
- https://docs.djangoproject.com/en/3.0/topics/testing/tools/#django.test.Client.post

# お読みいただきありがとうございました！🐰