【Django】複数のファイルをまとめてテストするとTransactionErrorやIntegurityErrorになってしまう事件について
仕事でテスト周りについて色々あったので備忘録として
例えば、こういうViewとModelがあったとして・・・
(まあ、こんなこと普通しないとは思いつつ簡単な例を出したく)
models.py
from typing import Dict from django.db import models from django.db import IntegrityError class Customer(models.Model): GENDER_CHOICES = ( (0, 'Male'), (1, 'Female'), (2, 'Other') ) name = models.TextField() email = models.EmailField(unique=True) birthday = models.DateField() gender = models.PositiveSmallIntegerField(choices=GENDER_CHOICES) @classmethod def create(cls, data: Dict) -> None: """フォームのデータから顧客を新規登録するメソッド dataの中身 { 'name': 'name', 'email': 'email@example.com', 'birthday': datetime.date(1990, 1, 1), 'gender': 0 } """ try: cls.objects.create( name=data.get('name'), email=data.get('email'), birthday=data.get('birthday'), gender=data.get('gender') ) except IntegrityError: raise
views.py
from django.http import HttpResponse from django.views import View from django.shortcuts import render from django import forms from django.db import IntegrityError from .models import Customer # ブログを簡素にしたいのでViewに書いたけどforms.pyに書いてもよし class CustomerForm(forms.Form): GENDER_CHOICES = ( (0, 'Male'), (1, 'Female'), (2, 'Other') ) name = forms.CharField(max_length=255) email = forms.EmailField() gender = forms.ChoiceField(choices=GENDER_CHOICES) class CreateCustomerView(View): def get(self, request): """顧客名一覧を表示 """ costomers = [ customer.name for customer in Customer.objects.iterator() ] return render( request, 'form.html', { 'costomers': costomers, 'form': CustomerForm() } ) def post(self, request): """顧客を新規登録 request.postの形式 { "name": "Taro Tanaka", "birthday": "1980-01-01", "gender": 0 } """ form = CustomerForm(request.POST) if form.is_valid(): try: Customer.create(data=form.cleaned_data) except IntegrityError: form.add('email', 'すでに登録しているメールアドレスは登録できません') return render( requset, 'form.html', { 'costomers': costomers, 'form': form } )
こうテストを書くと
test_models.py
import datetime from django.db import IntegrityError from django.test import TestCase from .models import Customer class TestCustomerModel(TestCase): @classmethod def setUpTestData(cls): Customer.objects.create( name='Taro Yamada', email='sample@example.com', birthday=datetime.date(1980, 1, 1), gender=0 ) def test_email_duplicate(self): data = { name: 'Jiro Yamada', email: 'sample@example.com', # すでに登録しているメールアドレス birthday: datetime.date(1980, 2, 2), gender: 0 } with self.assertRaises(IntegrityError): Customer.create(data=data) def test_create_valid_customer(self): data = { name: 'Hanako Takada', email: 'sample_2@example.com', birthday: datetime.date(1980, 3, 3), gender: 1 } Customer.create(data=data)
test_views.py
from django.test import TestCase, Client from .models import Customer from .views import CreateCustomerView class TestCustomerView(TestCase): @classmethod def setUpTestData(cls): Customer.objects.create( name='Taro Yamada', email='sample@example.com', birthday=datetime.date(1980, 1, 1), gender=0 ) def test_email_duplicate(self): data = { name: 'Jiro Yamada', email: 'sample@example.com', # すでに登録しているメールアドレス birthday: datetime.date(1980, 2, 2), gender: 0 } response = self.client.post( '', { "name": "Taro Yamada", "email": "sample@example.com", "gender": "0", "birthday": "1980-01-01", "position": "100", } ) self.assertContains( 'すでに登録しているメールアドレスは登録できません' response )
TransactionErrorが〜
IntegurityErrorが〜
・・・なぜ起こるか。
TestCaseを継承するクラスにsetUpClassとtearDownClassがないことに注目。
Djangoでは、setUpClassがそのテストクラスでテストを実行するための準備をするために、テストクラスが初期化される際に一度だけ呼ばれます。
tearDownClassがテストクラス内のテストがすべて終わりテストクラスが解放される際に一度だけ呼ばれる仕組みになっています。tearDownClassはsetUpClassが行った準備をすべてクリーンにするイメージかと思います。
DjangoのTestCaseを継承している場合、tearDownClassによって、トランザクションがロールバックされます。テスト用データベース内のデータがすべて一掃されます。
上記の例だと、
各テストクラスにsetUpClassとtearDownClassがないため、データベースのトランザクションが終わってない状態であるがゆえにTransactionError
最初に実行したテストクラスでのデータが残っているためユニークであるはずのデータが重複しようとしてしまい意図しないIntegurityError・・・
となってしまいました。
じゃあこうすればいいの!?
class TestCustomerModel(TestCase): @classmethod def setUpClass(cls): pass # 中略 ... @classmethod def tearDownClass(cls): pass
いいえ、違います。
ただ最初にsetUpClassとtearDownClassを追加しただけだとトランザクション開始と後処理の働きをしないので super()をつけることによってDjangoのTestCaseのsetUpClassとtearDownClassの働きを継承してあげましょう。
class TestCustomerModel(TestCase): @classmethod def setUpClass(cls): super().setUpClass() # 中略 ... @classmethod def tearDownClass(cls): super().tearDownClass()
他にもテストクラス内のテストメソッドは順番関係なく実行されてもOKなように・・・などありますが
他のファイルが影響するということは避けられるようになりました!
(Viewのテストは実際にトランザクション走らせずモック使っても・・・というのはまた別の記事で!)
<参考>