django.contrib.messagesAPIで送付したmessageの内容をdjango.test.TestCaseを継承したテストクラスで確認する


環境及びやりたかったこと

Python3.9.0
Django3.2

以下のようにユーザー登録用のビューを作った。
見たままなのだが、軽く解説すると入力されたデータをSignUpFormという自作のFormクラスで検証し、バリデーションが通ればユーザーを登録し、ルートパスにリダイレクトする、という挙動を行うクラスになっている。
また、リダイレクト後の画面で「ユーザー情報を登録しました。ご利用いただきありがとうございます。」というメッセージを表示したいので、SignUpFormのバリデーションが通った際に呼び出されるform_valid()をオーバーライドしてdjango.contrib.messages.info()を使ってメッセージを入力している。1

views.py
from django.views.generic import edit, View
from django.contrib import messages

from .models import User

class SignUpView(edit.CreateView):
  """ユーザー登録用ビュークラス"""
  model = User
  form_class = SignUpForm
  template_name = 'users/signup.html'
  success_url = '/'

  def form_valid(self, form):
    """ユーザー登録時にmessageを表示する"""
    messages.info(self.request, 'ユーザー情報を登録しました。ご利用いただきありがとうございます。')
    return response

このメッセージの内容を検証したいというのが今回やりたかったこと。

ルーティング、Userモデル、SignUpFormは以下のとおり。

urls.py
from django.urls import path

from .views import SignUpView

app_name = 'users'

urlpatterns = [
  path('signup/', SignUpView.as_view(), name='signup'),
]
models.py
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.utils import timezone
from django.db import models

class UserManager(BaseUserManager):
  use_in_migrations = True

  def _create_user(self, email, password, **kwargs):
    # emailを必須に
    if not email:
      raise ValueError('メールアドレスは必須です')
    email = self.normalize_email(email)

    user = self.model(email=email, **kwargs)
    user.set_password(password)
    user.save(using=self.db)
    return user

  def create_user(self, email, password=None, **kwargs):
    kwargs.setdefault('is_staff', False)
    kwargs.setdefault('is_superuser', False)
    return self._create_user(email, password, **kwargs)

  def create_superuser(self, email, password, **kwargs):
    kwargs.setdefault('is_staff', True)
    kwargs.setdefault('is_superuser', True)
    if kwargs.get('is_staff') is not True:
      raise ValueError('superuserのis_staffはTrueである必要があります')
    if kwargs.get('is_superuser') is not True:
      raise ValueError('superuserのis_superuserはTrueである必要があります')
    return self._create_user(email, password, **kwargs)



class User(AbstractBaseUser, PermissionsMixin):
  """ユーザーモデル"""
  class Meta:
    verbose_name_plural = 'ユーザー'

  email = models.EmailField(verbose_name='メールアドレス', unique=True)
  is_staff = models.BooleanField(verbose_name='is_staff', default=False)
  is_active = models.BooleanField(verbose_name='is_active', default=True)
  date_joined = models.DateTimeField(verbose_name='登録日', default=timezone.now)

  objects = UserManager()

  USERNAME_FIELD = "email"
  EMAIL_FIELD = "email"
  REQUIRED_FIELDS = []
forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm

from .models import User

class SignUpForm(UserCreationForm, UserFormMixin):
  """ユーザーモデル用フォーム"""
  class Meta:
    model = User
    fields = ['email']

テストコードは下記の通り。

test_views.py
from django.test import TestCase

from users.models import User

class TestSignUpView(TestCase):
  """SignUpViewのテストクラス"""

  def test_post_success(self):
      """
      /users/signup/ にPOSTリクエストすると
      ユーザー登録が成功することを検証
      """
      # テストクライアントでPOSTリクエストを送信
      response = self.client.post('/users/signup/',
      {
        'email': '[email protected]',
        'password1': 'examplepassword123',
        'password2': 'examplepassword123'
      })
      .
      .
      .
      # メッセージの内容を検証
      # message = 何らかの処理
      self.assertEqual(message, 'ユーザー情報を登録しました。ご利用いただきありがとうございます。')

ここでresponseからどのようにしてメッセージの内容を取り出し、messageに格納するかで少し詰まったので備忘録として残しておく。

解決した方法

test_views.py
from django.test import TestCase
from django.contrib.messages import get_messages # 追加

from users.models import User

class TestSignUpView(TestCase):
  """SignUpViewのテストクラス"""

  def test_post_success(self):
      """
      /users/signup/ にPOSTリクエストすると
      ユーザー登録が成功することを検証
      """
      # テストクライアントでPOSTリクエストを送信
      response = self.client.post('/users/signup/',
      {
        'email': '[email protected]',
        'password1': 'examplepassword123',
        'password2': 'examplepassword123'
      })
      .
      .
      .
      # メッセージの内容を検証
      messages = list(get_messages(response.wsgi_request)) #追加
      message = str(messages[0]) #追加
      self.assertEqual(message, 'ユーザー情報を登録しました。ご利用いただきありがとうございます。')

少し長くなったのでmessageの中身を決める処理を2行に分けた。

追加された2行に関する調査及び疑問点

よくわからないものをわからないまま使うのも姿勢としてよくないだろうと思ったのでこの2行で何をやってるのかをDjangoのソースコードとドキュメントを読んで調べた。
公式ドキュメントを見ると、DjangoのテストクライアントはHttpResponseオブジェクトを生成する際、送信されたリクエストの内容を表すHttpResponseオブジェクトをWSGIRequestというHttpResponseのサブクラスの型に変換し、それをresponse.wsgi_requestに格納するようだった。
django.contrib.messages.get_messages()はHttpResponseオブジェクトの中からFallbackStorageオブジェクトを取得する。
このFallbackStorageオブジェクトが普段メッセージストレージと呼ばれているものを提供するクラスのようだ。
なお、get_messages()はメッセージが存在しない場合は空のリストを返すのだが、メッセージが存在する場合に返されるFallbackStorageオブジェクトは添字に対応していない為、そのままget_messages()[0]として要素を取り出そうとすると、「TypeError: 'FallbackStorage' object is not subscriptable」と怒られる。
そのためlist()で変換しているので、次にFallbackStorageクラスの__iter__()2を確認したのだが、この処理が意味するところは私の力量が足りず理解できなかった。
結果としてはlist()にFallbackStorageオブジェクトを渡すことでMessageオブジェクトのリストが得られるのだが、Messageクラスの__str__()がMessageインスタンスのmessageプロパティを返すようになっているため、メッセージの内容を確認するにはただstr()に渡せばいいようだ。


  1. この場合はmessages.info()メソッドよりもmessages.success()メソッドの方が適切な気もするが、この記事を書いた時点では変更した際のテストをしていないので、info()のままにしておく。 

  2. 正確には__iter__()はFallbackStorageのスーパークラスであるBaseStorageクラスに定義されていた。