TransactionManagementError信号の使用中は「ユニットブロックの終わりまでクエリを実行できません」が、ユニットテスト中のみ


194

Django Userモデルのインスタンスを保存しようとするとTransactionManagementErrorが発生し、そのpost_save信号で、ユーザーを外部キーとして持つモデルをいくつか保存しています。

コンテキストとエラーは、シグナルを使用する場合のこの質問django TransactionManagementErrorにかなり似 ています

ただし、この場合、ユニットテスト中にのみエラーが発生します

手動テストではうまく機能しますが、単体テストは失敗します。

私が見逃しているものはありますか?

コードスニペットは次のとおりです。

views.py

@csrf_exempt
def mobileRegister(request):
    if request.method == 'GET':
        response = {"error": "GET request not accepted!!"}
        return HttpResponse(json.dumps(response), content_type="application/json",status=500)
    elif request.method == 'POST':
        postdata = json.loads(request.body)
        try:
            # Get POST data which is to be used to save the user
            username = postdata.get('phone')
            password = postdata.get('password')
            email = postdata.get('email',"")
            first_name = postdata.get('first_name',"")
            last_name = postdata.get('last_name',"")
            user = User(username=username, email=email,
                        first_name=first_name, last_name=last_name)
            user._company = postdata.get('company',None)
            user._country_code = postdata.get('country_code',"+91")
            user.is_verified=True
            user._gcm_reg_id = postdata.get('reg_id',None)
            user._gcm_device_id = postdata.get('device_id',None)
            # Set Password for the user
            user.set_password(password)
            # Save the user
            user.save()

signal.py

def create_user_profile(sender, instance, created, **kwargs):
    if created:
        company = None
        companycontact = None
        try:   # Try to make userprofile with company and country code provided
            user = User.objects.get(id=instance.id)
            rand_pass = random.randint(1000, 9999)
            company = Company.objects.get_or_create(name=instance._company,user=user)
            companycontact = CompanyContact.objects.get_or_create(contact_type="Owner",company=company,contact_number=instance.username)
            profile = UserProfile.objects.get_or_create(user=instance,phone=instance.username,verification_code=rand_pass,company=company,country_code=instance._country_code)
            gcmDevice = GCMDevice.objects.create(registration_id=instance._gcm_reg_id,device_id=instance._gcm_reg_id,user=instance)
        except Exception, e:
            pass

tests.py

class AuthTestCase(TestCase):
    fixtures = ['nextgencatalogs/fixtures.json']
    def setUp(self):
        self.user_data={
            "phone":"0000000000",
            "password":"123",
            "first_name":"Gaurav",
            "last_name":"Toshniwal"
            }

    def test_registration_api_get(self):
        response = self.client.get("/mobileRegister/")
        self.assertEqual(response.status_code,500)

    def test_registration_api_post(self):
        response = self.client.post(path="/mobileRegister/",
                                    data=json.dumps(self.user_data),
                                    content_type="application/json")
        self.assertEqual(response.status_code,201)
        self.user_data['username']=self.user_data['phone']
        user = User.objects.get(username=self.user_data['username'])
        # Check if the company was created
        company = Company.objects.get(user__username=self.user_data['phone'])
        self.assertIsInstance(company,Company)
        # Check if the owner's contact is the same as the user's phone number
        company_contact = CompanyContact.objects.get(company=company,contact_type="owner")
        self.assertEqual(user.username,company_contact[0].contact_number)

トレースバック:

======================================================================
ERROR: test_registration_api_post (nextgencatalogs.apps.catalogsapp.tests.AuthTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/gauravtoshniwal1989/Developer/Web/Server/ngc/nextgencatalogs/apps/catalogsapp/tests.py", line 29, in test_registration_api_post
    user = User.objects.get(username=self.user_data['username'])
  File "/Users/gauravtoshniwal1989/Developer/Web/Server/ngc/ngcvenv/lib/python2.7/site-packages/django/db/models/manager.py", line 151, in get
    return self.get_queryset().get(*args, **kwargs)
  File "/Users/gauravtoshniwal1989/Developer/Web/Server/ngc/ngcvenv/lib/python2.7/site-packages/django/db/models/query.py", line 301, in get
    num = len(clone)
  File "/Users/gauravtoshniwal1989/Developer/Web/Server/ngc/ngcvenv/lib/python2.7/site-packages/django/db/models/query.py", line 77, in __len__
    self._fetch_all()
  File "/Users/gauravtoshniwal1989/Developer/Web/Server/ngc/ngcvenv/lib/python2.7/site-packages/django/db/models/query.py", line 854, in _fetch_all
    self._result_cache = list(self.iterator())
  File "/Users/gauravtoshniwal1989/Developer/Web/Server/ngc/ngcvenv/lib/python2.7/site-packages/django/db/models/query.py", line 220, in iterator
    for row in compiler.results_iter():
  File "/Users/gauravtoshniwal1989/Developer/Web/Server/ngc/ngcvenv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 710, in results_iter
    for rows in self.execute_sql(MULTI):
  File "/Users/gauravtoshniwal1989/Developer/Web/Server/ngc/ngcvenv/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 781, in execute_sql
    cursor.execute(sql, params)
  File "/Users/gauravtoshniwal1989/Developer/Web/Server/ngc/ngcvenv/lib/python2.7/site-packages/django/db/backends/util.py", line 47, in execute
    self.db.validate_no_broken_transaction()
  File "/Users/gauravtoshniwal1989/Developer/Web/Server/ngc/ngcvenv/lib/python2.7/site-packages/django/db/backends/__init__.py", line 365, in validate_no_broken_transaction
    "An error occurred in the current transaction. You can't "
TransactionManagementError: An error occurred in the current transaction. You can't execute queries until the end of the 'atomic' block.

----------------------------------------------------------------------

ドキュメントから:「一方、TestCaseはテスト後にテーブルを切り捨てません。代わりに、テストの最後にロールバックされるデータベーストランザクションにテストコードを含めます。transaction.commitのような明示的なコミットは両方とも()およびtransaction.atomic()によって引き起こされる可能性のある暗黙的なものは、nop操作で置き換えられます。これにより、テスト終了時のロールバックでデータベースが初期状態に復元されることが保証されます。
Gaurav Toshniwal 14

6
問題が見つかりました。この「try:...例外IntegrityError:...」のようなIntegrityError例外がありました。私がしなければならなかったことは、try-block内でtransaction.atomicを使用することです:「try:with transaction.atomic():.. 。IntegrityErrorを除く:... "これですべてが正常に動作します。
caio 2014

docs.djangoproject.com/en/dev/topics/db/transactionsを検索し、「try / exceptブロックでアトミックなラッピングを行うと、整合性エラーの自然な処理が可能になります:」
CamHart

回答:


236

私自身もこれと同じ問題に遭遇しました。これは、意図的に例外をトリガーするユニットテストと相まって、Djangoの新しいバージョンでトランザクションがどのように処理されるかという癖が原因です。

IntegrityError例外を意図的にトリガーすることにより、一意の列制約が適用されていることを確認するためのユニットテストを行いました。

def test_constraint(self):
    try:
        # Duplicates should be prevented.
        models.Question.objects.create(domain=self.domain, slug='barks')
        self.fail('Duplicate question allowed.')
    except IntegrityError:
        pass

    do_more_model_stuff()

Django 1.4では、これは正常に機能します。ただし、Django 1.5 / 1.6では、各テストはトランザクションにラップされるため、例外が発生すると、明示的にロールバックするまでトランザクションが中断されます。したがって、そのトランザクションでのmyなどのその後のORM操作は、do_more_model_stuff()そのdjango.db.transaction.TransactionManagementError例外で失敗します。

コメントで言及されているcaioのように、解決策は次のようにして例外をキャプチャすることですtransaction.atomic

from django.db import transaction
def test_constraint(self):
    try:
        # Duplicates should be prevented.
        with transaction.atomic():
            models.Question.objects.create(domain=self.domain, slug='barks')
        self.fail('Duplicate question allowed.')
    except IntegrityError:
        pass

これにより、意図的にスローされた例外が単体テストのトランザクション全体を破壊するのを防ぎます。


70
また、テストクラスを単にTestCaseではなくTransactionTestCaseとして宣言することも検討してください。
mkoistinen

1
ああ、別の質問から関連ドキュメントを見つけました。ドキュメントはこちらです。
yaobin 2015年

2
私にとっては、私はすでに持っていたtransaction.atomic()ブロックを、私はこのエラーを持って、私はなぜわかりませんでした。私はこの回答のアドバイスを取り入れて、問題の領域の周りの私のアトミックブロック内にネストされたアトミックブロックを置きました。その後、ヒットした整合性エラーの詳細なエラーが表示され、コードを修正して、私がやろうとしていたことを実行できました。
AlanSE 16

5
@mkoistinen TestCaseはから継承しているTransactionTestCaseため、変更する必要はありません。テスト用にDBを操作しない場合SimpleTestCase
bnsは、

1
@bnsあなたはコメントのポイントを逃しています。はい、TestCase継承しますTransactionTestCaseが、その動作はまったく異なります。各テストメソッドをトランザクションでラップします。 TransactionTestCase一方、おそらく誤解を招くような名前が付けられています。テーブルを切り捨ててデータベースをリセットします。この名前は、テストがトランザクションとしてラップされているのではなく、テスト内でトランザクションをテストできることを反映しているようです。
CS

48

@mkoistinenが彼のコメントを返答しなかったため、コメントを掘り下げる必要がないように、私は彼の提案を投稿します。

TestCaseだけでなくTransactionTestCaseとしてテストクラスを宣言することを検討してください。

ドキュメントから:TransactionTestCaseはcommitとrollbackを呼び出し、これらの呼び出しがデータベースに及ぼす影響を観察できます。


2
+1ですが、ドキュメントにあるように、「DjangoのTestCaseクラスは、TransactionTestCaseのより一般的に使用されるサブクラスです」。元の質問に答えるために、TestCaseの代わりにSimpleTestCaseを使用するべきではありませんか?SimpleTestCaseには、アトミックデータベース機能はありません。
daigorocub 2016年

@daigorocubから継承するときSimpleTestCaseallow_database_queries = Trueそれは唾を吐くしないように、テストクラス内で追加する必要がありますAssertionError("Database queries aren't allowed in SimpleTestCase...",)
CristiFati 2017年

これは、整合性エラーをテストしようとしていたときに発生するエラーであり、その後、データベース保存クエリをさらに実行する必要があったので、私にとって最適な答えです
Kim Stacks

8

pytest-djangoを使用している場合transaction=Trueは、django_dbこのエラーを回避するためにデコレータに渡すことができます。

https://pytest-django.readthedocs.io/en/latest/database.html#testing-transactionsご覧ください

Django自体にはTransactionTestCaseがあり、これによりトランザクションをテストでき、テスト間でデータベースをフラッシュしてトランザクションを分離します。これの欠点は、データベースのフラッシュが必要なため、これらのテストのセットアップが非常に遅くなることです。pytest-djangoは、django_dbマークへの引数を使用して選択できるこのスタイルのテストもサポートしています。

@pytest.mark.django_db(transaction=True)
def test_spam():
    pass  # test relying on transactions

このソリューションに問題があり、DBに初期データがあった(移行によって追加された)。このソリューションはデータベースをフラッシュするため、この初期データに依存する他のテストは失敗し始めました。
abumalick

1

私にとって、提案された修正は機能しませんでした。私のテストでは、Popen移行を分析/制限するためにいくつかのサブプロセスを開きます(たとえば、1つのテストでモデルの変更がないかどうかを確認します)。

私にとっては、SimpleTestCase代わりにからサブクラス化することでTestCaseうまくいきました。

SimpleTestCaseデータベースを使用できないことに注意してください。

これは元の質問に答えるものではありませんが、とにかくこれが一部の人々に役立つことを願っています。


1

この質問への回答に基づいて、これを行う別の方法を次に示します。

with transaction.atomic():
    self.assertRaises(IntegrityError, models.Question.objects.create, **{'domain':self.domain, 'slug':'barks'})

0

django 1.9.7を使用してcreate_test_data関数でユニットテストを実行すると、このエラーが発生しました。それは以前のバージョンのdjangoで動作しました。

それはこのように見えました:

cls.localauth,_ = Organisation.objects.get_or_create(organisation_type=cls.orgtypeLA, name='LA for test', email_general='test@test.com', address='test', postcode='test', telephone='test')
cls.chamber,_ = Organisation.objects.get_or_create(organisation_type=cls.orgtypeC, name='chamber for test', email_general='test@test.com', address='test', postcode='test', telephone='test')
cls.lawfirm,_ = Organisation.objects.get_or_create(organisation_type=cls.orgtypeL, name='lawfirm for test', email_general='test@test.com', address='test', postcode='test', telephone='test')

cls.chamber.active = True
cls.chamber.save()

cls.localauth.active = True
cls.localauth.save()    <---- error here

cls.lawfirm.active = True
cls.lawfirm.save()

私の解決策は、代わりにupdate_or_createを使用することでした:

cls.localauth,_ = Organisation.objects.update_or_create(organisation_type=cls.orgtypeLA, name='LA for test', email_general='test@test.com', address='test', postcode='test', telephone='test', defaults={'active': True})
cls.chamber,_ = Organisation.objects.update_or_create(organisation_type=cls.orgtypeC, name='chamber for test', email_general='test@test.com', address='test', postcode='test', telephone='test', defaults={'active': True})
cls.lawfirm,_ = Organisation.objects.update_or_create(organisation_type=cls.orgtypeL, name='lawfirm for test', email_general='test@test.com', address='test', postcode='test', telephone='test', defaults={'active': True})

1
get_or_create()同様に動作します、それはそれがtransaction.atomic()装飾された関数内で好きではない.save()であるようです(私の中では1回の呼び出しで失敗しました)。
ティモシーマコブ2016年

0

同じ問題がwith transaction.atomic()ありますが、TransactionTestCaseうまくいきませんでした。

python manage.py test -r代わりにpython manage.py test、おそらく実行の順序が重要である、私のためokです

次に、テストが実行される順序に関するドキュメントを見つけます。どのテストが最初に実行されるかが記載されています。

だから、私はデータベースの相互作用にTestCaseを使用しunittest.TestCase、他の簡単なテストでは、それが今すぐ機能します!


0

@kdazzleの答えは正しいです。「DjangoのTestCaseクラスはTransactionTestCaseのより一般的に使用されるサブクラスである」と人々が言っ​​たので、私はそれを試しませんでした。しかしJahongir Rahmonovブログはそれをよりよく説明しました:

TestCaseクラスは、2つのネストされたatomic()ブロック内でテストをラップします。1つはクラス全体用で、もう1つは各テスト用です。ここでTransactionTestCaseを使用する必要があります。atomic()ブロックでテストをラップしないため、トランザクションを必要とする特別なメソッドを問題なくテストできます。

編集:それはうまくいかなかった、私はそう思ったが、いいえ。

4年間で彼らはこれを修正することができました.................................................


0
def test_wrong_user_country_db_constraint(self):
        """
        Check whether or not DB constraint doesnt allow to save wrong country code in DB.
        """
        self.test_user_data['user_country'] = 'XX'
        expected_constraint_name = "country_code_within_list_of_countries_check"

        with transaction.atomic():
            with self.assertRaisesRegex(IntegrityError, expected_constraint_name) as cm:
                get_user_model().objects.create_user(**self.test_user_data)

        self.assertFalse(
            get_user_model().objects.filter(email=self.test_user_data['email']).exists()
        )
with transaction.atomic() seems do the job correct

-4

同じ問題がありました。

私の場合、私はこれをしていました

author.tasks.add(tasks)

に変換する

author.tasks.add(*tasks)

そのエラーを削除しました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.