overview
Djangoで開発していると、トランザクションを張る部分が出てきます。
そして、「このトランザクションがコミットされたらアクションを起こしたいな…」というニーズもあるはずです。
そんな時役に立つのがtransaction.on_commit。
from django.db import transaction
def do_something():
pass
transaction.on_commit(do_something)
※ lambdaでラップすれば変数を渡すこともできる。 e.g. transaction.on_commit(lambda: do_something(arg))
通常はメール送信やcelery taskなど、投げっぱなしのactionで使用することが多いためテストを実装するというパターンは少ないかもしれない。
しかし、実際にテストするとなると簡単にはいかないのだ…
訪れるAssertionError
(検証用だからゴミコード許して)
from django.db import transaction
from .models import Animal
def do_something(instance):
instance.has_changed = True
instance.save()
@transaction.atomic
def animal_create(name):
animal = Animal()
animal.name = name
animal.save()
transaction.on_commit(lambda: do_something(animal))
Animalを生成するコードがあったとします
from django.test import TestCase
from .views import animal_create
from .models import Animal
class AnimalCreateTestCase(TestCase):
def test_animal_create(self):
animal_create('mouse')
self.assertEqual(1, Animal.objects.count())
animal = Animal.objects.first()
self.assertEqual('mouse', animal.name)
self.assertEqual(True, animal.has_changed)
それをテストします。
$ ./manage.py test
======================================================================
FAIL: test_animal_create (app.tests.AnimalCreateTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/takino/PycharmProjects/workshop/django_4_0/app/tests.py", line 12, in test_animal_create
self.assertEqual(True, animal.has_changed)
AssertionError: True != False
こけます
なぜ?
- on_commitはcommit後に実行される
- TestCaseはtransactionレベルで実行され、テスト終了後にrollbackされるためtransactionはcommitされない
- commitされないということはon_commitが走らない
じゃあどうするの
TransactionTestCaseを使用する
from django.test import TransactionTestCase
from .views import animal_create
from .models import Animal
class AnimalCreateTestCase(TransactionTestCase):
def test_animal_create(self):
animal_create('mouse')
self.assertEqual(1, Animal.objects.count())
animal = Animal.objects.first()
self.assertEqual('mouse', animal.name)
self.assertEqual(True, animal.has_changed)
テストは成功します
$ ./manage.py test
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.029s
OK
TransactionTestCase vs TestCase
TestCase
- テストはトランザクション内にラップされる
- テスト実行後はロールバックされる
- コミットされないのでon_commitは走らない
TransactionTestCase
- トランザクションは作成しない
- テスト実行後はDBをフラッシュする
- コミットをするのでon_commitが走る
- テスト間でDBのフラッシュが行われるため、TestCaseより低速
最後に
あたりまえのようにTestCaseを使っていると意外と気づかないものだった。
別スレッドの処理でもTransactionTestCaseを使う必要があるため、
実装内容によってどのテストクラスを使用するべきかなどの検討はちゃんとしなければならない。
まあ、本記事はDjangerにとっては当たり前の内容だと思うので、備忘録的に記事を残しつつキーボードを止めることにする。