LoginSignup
6
6

More than 1 year has passed since last update.

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

(検証用だからゴミコード許して)

views.py
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を生成するコードがあったとします

tests.py
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

こけます :innocent:

なぜ?

  • on_commitはcommit後に実行される
  • TestCaseはtransactionレベルで実行され、テスト終了後にrollbackされるためtransactionはcommitされない
  • commitされないということはon_commitが走らない :innocent:

じゃあどうするの

TransactionTestCaseを使用する

tests.py
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)

テストは成功します :thumbsup:

$ ./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にとっては当たり前の内容だと思うので、備忘録的に記事を残しつつキーボードを止めることにする。

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6