unittestに関するメモ〜postメソッド編

今回のお題
-
djangoのunittestを用いてpostメソッド回りの検証をしていた時に起こったことをメモしておきます。

基本的には自分用です。

postメソッドとは
-
unittestにおいて、POSTメソッドでリクエストを送るためのメソッド。

```python:postメソッドの例
# 型
self.client.post("リクエスト先のパス", "送信するパラメータ")

# 例
self.client.post("/book/update/", {
  "title": "カラマーゾフの兄弟",
  "price": 1500
  })
```

リクエスト先のパスの指定には`reverseメソッドを用いてもOK。

また、送信するパラメータは辞書形式で指定する。

上記の例では、"/book/update/"というurlに対して`{ "title": "カラマーゾフの兄弟", "price": 1500 }`というパラメータが送信されている。

postメソッドを用いる際の注意点１〜値が変わらないフィールドの取り扱い
-
レコードの更新のためにpostメソッドを用いる場合、変更が入らない属性値であっても省略せずに値を指定すること。

省略した属性に関してはフォームが空の状態でリクエストを送信した扱いになり、フィールド値が意図しない形で上書きされたり、`null=False`や`blank=false`オプションに弾かれてレコードの更新そのものが無効になったりする。

具体例は以下参照。

```python:モデル
class Book(models.Model):
  title = models.CharField(max_length=30)
  price = models.IntegerField()
  explain = models.TextField(null=True, blank=True)
````

```python:フォーム
class BookForm(ModelForm):
  class Meta:
    model = Book
    fields = "__all__"
```

```python:ルーティング
# プロジェクト側
urlpatterns = [
  path("book/", include(("book.urls", "book"))),
]

# アプリケーション側
urlpatterns = [
  path("update/<int:pk>/", views.BookUpdateView.as_view(), name="update"),
]
```

````python:テスト
class TestBookUpdate(TestCase):
  def setUp(self):
    self.book = Book.objects.create(
      title="カラマーゾフの兄弟",
      price=1000,
      explain="ドストエフスキーの代表作です",
    )
    self.path = reverse("book:update", kwargs={"pk": self.book.pk})
    return super().setUp()

  def tearDown(self):
    Book.objetcs.all().delete()
    return super().tearDown()

  # これはOK
  def test_post(self):
    self.client.post(cls.path,{
      "title": "罪と罰",
      "price": 1500,
      "explain": "カラマーゾフの兄弟と並ぶ代表作です",
    })
    
    book = Book.objects.first()
    print(book.title, book.price, book.explain)
    >> "罪と罰", 1500, "カラマーゾフの兄弟と並ぶ代表作です"

  # これはNG
  def test_post(self):
    self.client.post(cls.path,{
      "title": "罪と罰",
      # "price": 1500,
      "explain": "カラマーゾフの兄弟と並ぶ代表作です",
    })
    # 以下と同じ扱い
    # self.client.post(cls.path,{
    #   "title": "罪と罰",
    #   "price": "",
    #   "explain": "カラマーゾフの兄弟と並ぶ代表作です",
    # })
   
    # priceのnull=Falseオプションに引っかかるので、レコードの更新自体が無効
    book = Book.objects.first()
    print(book.title, book.price, book.explain)
    >> "カラマーゾフの兄弟", 1000, "ドストエフスキーの並ぶ代表作です"

  # これはOK
  def test_post(self):
    self.client.post(cls.path,{
      "title": "罪と罰",
      "price": 1000,
      "explain": "カラマーゾフの兄弟と並ぶ代表作です",
    })
    
    book = Book.objects.first()
    print(book.title, book.price, book.explain)
    >> "罪と罰", 1000, "カラマーゾフの兄弟と並ぶ代表作です"

  # これもOK
  self.client.post(cls.path,{
      "title": "罪と罰",
      "price": 1500,
      # "explain": "カラマーゾフの兄弟と並ぶ代表作です",
    })

  # 以下と同じ扱い
  # self.client.post(cls.path,{
  #     "title": "罪と罰",
  #     "price": 1500,
  #     "explain": "",
  #   })

  book = Book.objects.first()
  print(book.title, book.price, book.explain)
  >> "罪と罰", 1500, ""
```

:::note info
フォームに設定していないフィールドに関しては、postメソッドで値を送信する必要はない。
元になったインスタンスから値が自動で取得される。
:::

postメソッドの注意点２〜外部キーに関する扱い
-
外部キーが設定されているフィールドに関しては、そのキーのprimary_keyの値を送信する。

```python:モデル設計
class Author(models.Model):
  name = models.CharField(max_length=30)

class Book(models.Model):
  title = models.CharField(max_length=30)
  price = models.IntegerField()
  explain = models.TextField(null=True, blank=True)
  author = models.ForeignKey(Author, on_delete=models.CASCADE, null=True)
```

```python
book = Book.objects.first()
author = Author.objects.exclude(pk=book.author.pk).first()
# これはOK
self.client.post(reverse("book:update", kwargs={"pk": book.pk}),{
  "author": author.pk,
})

# これはNG(author=Noneになる)
self.client.post(reverse("book:update", kwargs={"pk": book.pk}),{
  "author": author,
})
```

また、外部キーを設定したくない場合には、postの値としては空文字を指定し、assertEqualの引数としてはNoneを指定する。

```python
book = Book.objects.first()
self.client.post(reverse("book:update", kwargs={"pk": book.pk}),{
  "author": "",
})

book_after_update = Book.objects.get(pk=book.pk)
# これはOK
self.assertEqual(book_after_update.author, None)

# これはNG
self.assertEqual(book_after_update.author, "")

# 以下の2つもNG
self.assertEqual(book_after_update.author.pk, "")
self.assertEqual(book_after_update.author.pk, None)
>> AttributeError: 'NoneType' object has no attribute 'pk'

# これならOK
self.assertEqual(book_after_update.author_id, None)
# これはNG
self.assertEqual(book_after_update.author_id, "")
```