TransactionManagementError "Sie können keine Abfragen bis zum Ende des 'atomaren' Blocks ausführen", während Sie Signale verwenden, sondern nur während des Komponententests

194

Ich erhalte TransactionManagementError, wenn ich versuche, eine Django-Benutzermodellinstanz zu speichern, und in ihrem post_save-Signal speichere ich einige Modelle, die den Benutzer als Fremdschlüssel haben.

Der Kontext und Fehler ist dieser Frage ziemlich ähnlich. Django TransactionManagementError bei der Verwendung von Signalen

In diesem Fall tritt der Fehler jedoch nur beim Testen der Einheit auf .

Es funktioniert gut beim manuellen Testen, aber Unit-Tests schlagen fehl.

Fehlt mir etwas?

Hier sind die Codefragmente:

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)

Zurück verfolgen:

======================================================================
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.

----------------------------------------------------------------------
Gaurav Toshniwal
quelle
Aus den Dokumenten: "Ein TestCase hingegen schneidet Tabellen nach einem Test nicht ab. Stattdessen schließt er den Testcode in eine Datenbanktransaktion ein, die am Ende des Tests zurückgesetzt wird. Beide expliziten Festschreibungen erfolgen wie transaction.commit () und implizite, die möglicherweise durch transaction.atomic () verursacht werden, werden durch eine NOP-Operation ersetzt. Dies garantiert, dass der Rollback am Ende des Tests die Datenbank in ihren ursprünglichen Zustand zurückversetzt. "
Gaurav Toshniwal
6
Ich habe mein Problem gefunden. Es gab eine IntegrityError-Ausnahme wie diese "try: ... außer IntegrityError: ...". Ich musste lediglich die transaction.atomic im try-Block verwenden: "try: with transaction.atomic (): .. . außer IntegrityError: ... "jetzt funktioniert alles einwandfrei.
Caio
docs.djangoproject.com/de/dev/topics/db/transactions und suchen Sie dann nach "Das Einwickeln von Atomic in einen Try / Except- Block ermöglicht die natürliche Behandlung von Integritätsfehlern:"
CamHart

Antworten:

236

Ich bin selbst auf dasselbe Problem gestoßen. Dies wird durch eine Eigenart in der Art und Weise verursacht, wie Transaktionen in den neueren Versionen von Django behandelt werden, verbunden mit einer Unittest, die absichtlich eine Ausnahme auslöst.

Ich hatte eine Unittest, die überprüfte, ob eine eindeutige Spalteneinschränkung durch gezieltes Auslösen einer IntegrityError-Ausnahme erzwungen wurde:

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()

In Django 1.4 funktioniert dies einwandfrei. In Django 1.5 / 1.6 wird jedoch jeder Test in eine Transaktion eingeschlossen. Wenn also eine Ausnahme auftritt, wird die Transaktion unterbrochen, bis Sie sie explizit zurücksetzen. Daher schlagen alle weiteren ORM-Vorgänge in dieser Transaktion, wie z. B. my do_more_model_stuff(), mit dieser django.db.transaction.TransactionManagementErrorAusnahme fehl .

Wie in den Kommentaren erwähnt, besteht die Lösung darin, Ihre Ausnahme transaction.atomicwie folgt zu erfassen :

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

Dadurch wird verhindert, dass die absichtlich ausgelöste Ausnahme die gesamte Transaktion der Unittest unterbricht.

Cerin
quelle
70
Denken Sie auch daran, Ihre Testklasse nur als TransactionTestCase und nicht nur als TestCase zu deklarieren.
mkoistinen
1
Oh, ich habe das zugehörige Dokument aus einer anderen Frage gefunden . Das Dokument ist hier .
Yaobin
2
Für mich hatte ich bereits einen transaction.atomic()Block, aber ich bekam diesen Fehler und ich hatte keine Ahnung warum. Ich nahm den Rat dieser Antwort an und legte einen verschachtelten Atomblock in meinen Atomblock um den Problembereich. Danach gab es einen detaillierten Fehler des Integritätsfehlers, den ich traf, sodass ich meinen Code reparieren und das tun konnte, was ich versuchte.
AlanSE
5
@mkoistinen TestCaseerbt von TransactionTestCaseso keine Notwendigkeit, das zu ändern. Wenn Sie im Test nicht mit DB arbeiten SimpleTestCase.
Bns
1
@bns Sie verpassen den Punkt des Kommentars. Ja TestCaseerbt von, TransactionTestCaseaber sein Verhalten ist ganz anders: Es umschließt jede Testmethode in einer Transaktion. TransactionTestCaseAndererseits wird es möglicherweise irreführend benannt: Es schneidet Tabellen ab, um die Datenbank zurückzusetzen - die Benennung scheint zu reflektieren, dass Sie Transaktionen innerhalb eines Tests testen können, nicht, dass der Test als Transaktion verpackt ist!
CS
48

Da @mkoistinen nie seinen Kommentar abgegeben hat , eine Antwort, werde ich seinen Vorschlag posten, damit die Leute nicht durch Kommentare graben müssen.

Deklarieren Sie Ihre Testklasse nur als TransactionTestCase und nicht nur als TestCase.

Aus den Dokumenten : Ein TransactionTestCase kann Commit und Rollback aufrufen und die Auswirkungen dieser Aufrufe auf die Datenbank beobachten.

kdazzle
quelle
2
+1 dafür, aber wie in den Dokumenten angegeben, ist "Djangos TestCase-Klasse eine häufiger verwendete Unterklasse von TransactionTestCase". Sollten wir zur Beantwortung der ursprünglichen Frage nicht SimpleTestCase anstelle von TestCase verwenden? SimpleTestCase verfügt nicht über die Funktionen der Atomdatenbank.
Daigorocub
@daigorocub Wenn erben SimpleTestCase, allow_database_queries = Truemüssen innerhalb der Testklasse hinzugefügt werden, so dass es keine nicht spucken AssertionError("Database queries aren't allowed in SimpleTestCase...",).
CristiFati
Dies ist die Antwort, die für mich am besten funktioniert, da ich versucht habe, den Integritätsfehler zu testen. Anschließend wird mehr Datenbankabfragen ausgeführt
Kim Stacks,
8

Wenn Sie Pytest-Django verwenden, können Sie bestehen transaction=True an die übergebendjango_db Dekorateur übergeben, um diesen Fehler zu vermeiden.

Siehe https://pytest-django.readthedocs.io/en/latest/database.html#testing-transactions

Django selbst verfügt über den TransactionTestCase, mit dem Sie Transaktionen testen und die Datenbank zwischen den Tests leeren können, um sie zu isolieren. Der Nachteil dabei ist, dass diese Tests aufgrund des erforderlichen Löschens der Datenbank viel langsamer eingerichtet werden. pytest-django unterstützt auch diesen Teststil, den Sie mithilfe eines Arguments für die Marke django_db auswählen können:

@pytest.mark.django_db(transaction=True)
def test_spam():
    pass  # test relying on transactions
frmdstryr
quelle
Ich hatte ein Problem mit dieser Lösung. Ich hatte anfängliche Daten in meiner Datenbank (hinzugefügt durch Migrationen). Diese Lösung löscht die Datenbank, sodass andere Tests, die von diesen Anfangsdaten abhängen, fehlschlagen.
Abumalick
1

Für mich haben die vorgeschlagenen Korrekturen nicht funktioniert. In meinen Tests öffne ich einige Unterprozesse mitPopen , um Migrationen zu analysieren / fusseln (z. B. prüft ein Test, ob es keine Modelländerungen gibt).

Für mich hat die Unterklasse von SimpleTestCasestatt TestCaseden Trick getan.

Beachten Sie, dass SimpleTestCasedie Datenbank nicht verwendet werden kann.

Dies beantwortet zwar nicht die ursprüngliche Frage, aber ich hoffe, dass dies einigen Menschen trotzdem hilft.

flix
quelle
1

Hier ist eine andere Möglichkeit, basierend auf der Antwort auf diese Frage:

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

Ich habe diesen Fehler beim Ausführen von Komponententests in meiner Funktion create_test_data mit django 1.9.7 erhalten. Es hat in früheren Versionen von Django funktioniert.

Es sah so aus:

cls.localauth,_ = Organisation.objects.get_or_create(organisation_type=cls.orgtypeLA, name='LA for test', email_general='[email protected]', address='test', postcode='test', telephone='test')
cls.chamber,_ = Organisation.objects.get_or_create(organisation_type=cls.orgtypeC, name='chamber for test', email_general='[email protected]', address='test', postcode='test', telephone='test')
cls.lawfirm,_ = Organisation.objects.get_or_create(organisation_type=cls.orgtypeL, name='lawfirm for test', email_general='[email protected]', 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()

Meine Lösung bestand darin, stattdessen update_or_create zu verwenden:

cls.localauth,_ = Organisation.objects.update_or_create(organisation_type=cls.orgtypeLA, name='LA for test', email_general='[email protected]', 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='[email protected]', 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='[email protected]', address='test', postcode='test', telephone='test', defaults={'active': True})
PhoebeB
quelle
1
get_or_create()funktioniert auch, es scheint, dass es die .save () ist, die es in einer mit transaction.atomic () dekorierten Funktion nicht mag (meine ist mit nur 1 Aufruf fehlgeschlagen).
Timothy Makobu
0

Ich habe das gleiche Problem, aber with transaction.atomic()undTransactionTestCase nicht für mich arbeiten.

python manage.py test -r anstatt python manage.py test ist für mich in Ordnung, vielleicht ist die Reihenfolge der Ausführung entscheidend

dann finde ich ein Dokument über die Reihenfolge, in der Tests ausgeführt werden werden. Es erwähnt, welcher Test zuerst ausgeführt wird.

Also benutze ich TestCase für die Datenbankinteraktion, unittest.TestCasefür andere einfache Tests funktioniert es jetzt!

Löwe
quelle
0

Die Antwort von @kdazzle ist richtig. Ich habe es nicht ausprobiert, weil die Leute sagten, dass 'Djangos TestCase-Klasse eine häufiger verwendete Unterklasse von TransactionTestCase ist', also dachte ich, dass es die gleiche Verwendung ist. Aber der Blog von Jahongir Rahmonov erklärte es besser:

Die TestCase-Klasse fasst die Tests in zwei verschachtelte atomare () Blöcke zusammen: einen für die gesamte Klasse und einen für jeden Test. Hier sollte TransactionTestCase verwendet werden. Die Tests werden nicht mit dem atomaren () Block umschlossen, sodass Sie Ihre speziellen Methoden, für die eine Transaktion erforderlich ist, problemlos testen können.

EDIT: Es hat nicht funktioniert, ich dachte ja, aber nein.

In 4 Jahren konnten sie dies beheben .......................................

Shil Nevado
quelle
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
Aleksei Khatkevich
quelle
-4

Ich hatte das gleiche Problem.

In meinem Fall habe ich das getan

author.tasks.add(tasks)

also konvertiere es zu

author.tasks.add(*tasks)

Dieser Fehler wurde behoben.

Diaa Mohamed Kasem
quelle