【Django】フォームのカスタムバリデーションをテストする
Djangoのフォームで独自のバリデーションを実装した際のテスト方法を紹介します。 間違いがありましたら、ご指摘お願いします(>人<)
今回使ったバージョン
* Python 3.7.1 * Django 2.1.7
今回実装するフォームはこちら
forms.py
import re from django import forms from django.core.exceptions import ValidationError def is_hiragana(value): pattern = r'^[ぁ-ん]+$' if not re.fullmatch(pattern, value): raise ValidationError('ひらがなで書いてください') def validate_chatwork_id(value): pattern = r'^[0-9]+$' if not re.fullmatch(pattern, value) or len(value) != 7: raise ValidationError('半角数字7文字で書いてください') class EmployeeForm(forms.Form): name = forms.CharField( max_length=225, label='名前', widget=forms.TextInput(attrs={'class': 'form-control'}) ) name_kana = forms.CharField( max_length=225, label='名前(かな)', validators=[is_hiragana], required=False, widget=forms.TextInput(attrs={'class': 'form-control'}) ) chatwork_id = forms.CharField( label='チャットワークID', validators=[validate_chatwork_id], widget=forms.TextInput(attrs={'class': 'form-control'}) )
テストしたいこと
名前(かな)にひらがな以外が含まれていたらエラーを表示したい
チャットワークIDが半角数字7文字以外だったらエラーを表示したい
form.is_valid()でTrueかFalseを返すテストは良くみるんだけど、
正しくエラーメッセージが出力されるかの方法がわからん・・・
とりあえず書いてみた。
from django.core.exceptions import ValidationError from django.test import TestCase from .forms import EmployeeForm class TestEmployeeForm(TestCase): @classmethod def setUpClass(cls): pass def test_invalid_form_not_hiragana(self): params = dict( name='サンプル太郎', name_kana='サンプルタロウ', chatwork_id='0123456' ) form = EmployeeForm(params) form.is_valid() self.assertEqual( 'ひらがなで書いてください', form.errors ) @classmethod def tearDownClass(cls): pass
結果、怒られる。
AssertionError: 'ひらがなで書いてください' != {'name_kana': ['ひらがなで書いてください']}
Dict[List]か・・・なら・・・、と試しにやってみる。
def test_invalid_form_not_hiragana(self): params = dict( first_name='サンプル太郎', first_name_kana='サンプルタロウ', chatwork_id='0123456' ) form = EmployeeForm(params) form.is_valid() self.assertTrue( 'ひらがなで書いてください' in form.errors['name_kana'] )
できた!!!
同じ要領でchatwork_idもやってみると
def test_invalid_form_chatwork_id(self): with self.subTest('半角数字以外を含むとエラーになる'): params = dict( first_name='サンプル太郎', first_name_kana='さんぷるたろう', chatwork_id='012aaaaa' ) form = EmployeeForm(params) form.is_valid() self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] ) with self.subTest('7文字以上だとエラーになる'): params = dict( first_name='サンプル太郎', first_name_kana='さんぷるたろう', chatwork_id='012345678' ) form = EmployeeForm(params) form.is_valid() self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] ) with self.subTest('7文字以内だとエラーになる'): params = dict( first_name='サンプル太郎', first_name_kana='さんぷるたろう', chatwork_id='012345' ) form = EmployeeForm(params) form.is_valid() self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] )
form.errorsが何者かみてみると・・・
print(type(form.errors)) # django.forms.utils.ErrorDictが出力される print(type(form.errors['name_kana'])) # django.forms.utils.ErrorListが出力される print(form.errors) # <ul class="errorlist"><li>last_name<ul class="errorlist"><li>この項目は必須です。</li></ul></li><li>name_kana<ul class="errorlist"><li>ひらがなで書いてください</li></ul></li></ul> print(form.errors['name_kana']) # <ul class="errorlist"><li>ひらがなで書いてください</li></ul>
form.errorsはErrorDictというDictを継承したオブジェクトで
form.errorsはErrorListというListを継承したオブジェクトのようです。
ErrorDictのkeyはそのフォームの各フィールドになっているそうです。 ErrorListは各フィールドのエラーの表示方法を決めてるっぽいですね。
print()でulタグが出力されたのは、__str__の処理の中がas_ulだからかと謎が解けました。
https://github.com/django/django/blob/master/django/forms/utils.py
少しテストから話が逸れますが validatorsを使わずclean_field使用してもテストは同じ結果で通りました。
class EmployeeForm(forms.Form): name = forms.CharField( max_length=225, label='名前', widget=forms.TextInput(attrs={'class': 'form-control'}) ) name_kana = forms.CharField( max_length=225, label='名前(かな)', required=False, widget=forms.TextInput(attrs={'class': 'form-control'}) ) chatwork_id = forms.CharField( label='チャットワークID', widget=forms.TextInput(attrs={'class': 'form-control'}) ) def clean_name_kana(self): pattern = r'^[ぁ-ん]+$' name_kana = self.cleaned_data['name_kana'] if not re.fullmatch(pattern, name_kana): raise ValidationError('ひらがなで書いてください') return name_kana def clean_chatwork_id(self): pattern = r'^[0-9]+$' chatwork_id = self.cleaned_data['chatwork_id'] if not re.fullmatch(pattern, chatwork_id) or len(chatwork_id) != 7: raise ValidationError('半角数字7文字で書いてください') return chatwork_id
ちなみに、validatorとclean_fieldの違いについてはこちらの記事をご参照ください。 validatorはそれぞれのフィールドに入力された値をチェックするけれどもフォーム自体の内容は確認しないっぽいですね。
cleanが通らない場合があると考えると、clean_fieldを使った方が無難そう
今回使用したコードまとめ
forms.py
import re from django import forms from django.core.exceptions import ValidationError def is_hiragana(value): pattern = r'^[ぁ-ん]+$' if not re.fullmatch(pattern, value): raise ValidationError('ひらがなで書いてください') def validate_chatwork_id(value): pattern = r'^[0-9]+$' if not re.fullmatch(pattern, value) or len(value) != 7: raise ValidationError('半角数字7文字で書いてください') class EmployeeForm(forms.Form): name = forms.CharField( max_length=225, label='名前', widget=forms.TextInput(attrs={'class': 'form-control'}) ) name_kana = forms.CharField( max_length=225, label='名前(かな)', validators=[is_hiragana], required=False, widget=forms.TextInput(attrs={'class': 'form-control'}) ) chatwork_id = forms.CharField( label='チャットワークID', validators=[validate_chatwork_id], widget=forms.TextInput(attrs={'class': 'form-control'}) ) """こっちでも良い class EmployeeForm(forms.Form): name = forms.CharField( max_length=225, label='名前', widget=forms.TextInput(attrs={'class': 'form-control'}) ) name_kana = forms.CharField( max_length=225, label='名前(かな)', required=False, widget=forms.TextInput(attrs={'class': 'form-control'}) ) chatwork_id = forms.CharField( label='チャットワークID', widget=forms.TextInput(attrs={'class': 'form-control'}) ) def clean_name_kana(self): pattern = r'^[ぁ-ん]+$' name_kana = self.cleaned_data['name_kana'] if not re.fullmatch(pattern, name_kana): raise ValidationError('ひらがなで書いてください') return name_kana def clean_chatwork_id(self): pattern = r'^[0-9]+$' chatwork_id = self.cleaned_data['chatwork_id'] if not re.fullmatch(pattern, chatwork_id) or len(chatwork_id) != 7: raise ValidationError('半角数字7文字で書いてください') return chatwork_id """
test_forms.py(ちょっとテスト増やして整えた)
from django.core.exceptions import ValidationError from django.test import TestCase from .forms import EmployeeForm class TestEmployeeForm(TestCase): @classmethod def setUpClass(cls): pass def test_valid_form(self): params = dict( name='サンプル太郎', name_kana='さんぷるたろう', chatwork_id='0123456' ) form = EmployeeForm(params) self.assertTrue(form.is_valid()) def test_invalid_form_not_hiragana(self): params = dict( first_name='サンプル太郎', first_name_kana='サンプルタロウ', chatwork_id='0123456' ) form = EmployeeForm(params) form.is_valid() with self.subTest('フォームのバリーデーションで妥当ではないと返される'): self.assertFalse(form.is_valid()) with self.subTest('ひらがなで書けというエラーになる'): self.assertTrue( 'ひらがなで書いてください' in form.errors['name_kana'] ) self.assertTrue( 'ひらがなで書いてください' in form.errors['name_kana'] ) def test_invalid_form_chatwork_id_has_charactor(self): params = dict( name='サンプル太郎', name_kana='さんぷるたろう', chatwork_id='012aaaaa' ) form = EmployeeForm(params) form.is_valid() with self.subTest('半角数字以外を含むとフォームのバリーデーションで妥当ではないと返される'): self.assertFalse(form.is_valid()) with self.subTest('半角数字以外を含むと半角数字7文字で書けというエラーになる'): self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] ) def test_invalid_form_chatwork_id_is_over_7(self): params = dict( name='サンプル太郎', name_kana='さんぷるたろう', chatwork_id='012345678' ) form = EmpForm(params) form.is_valid() with self.subTest('7文字以上だとフォームのバリーデーションで妥当ではないと返される'): self.assertFalse(form.is_valid()) with self.subTest('7文字以上だと半角数字7文字で書けというエラーになる'): self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] ) def test_invalid_form_chatwork_id_is_less_than_7(self): params = dict( name='サンプル太郎', name_kana='さんぷるたろう', chatwork_id='012345' ) form = EmpForm(params) form.is_valid() with self.subTest('7文字以内だとフォームのバリーデーションで妥当ではないと返される'): self.assertFalse(form.is_valid()) with self.subTest('7文字以内だと半角数字7文字で書けというエラーになる'): self.assertTrue( '半角数字7文字で書いてください' in form.errors['chatwork_id'] ) @classmethod def tearDownClass(cls): pass