web-dev-qa-db-fra.com

TransactionManagementError "Vous ne pouvez pas exécuter de requêtes avant la fin du bloc" atomique "" lors de l'utilisation de signaux, mais uniquement pendant le test d'unité

Je reçois TransactionManagementError lorsque j'essaie de sauvegarder une instance de modèle Django User et que, dans son signal post_save, j'enregistre certains modèles dont l'utilisateur est la clé étrangère. 

Le contexte et l'erreur sont assez similaires à cette question Django TransactionManagementError lors de l'utilisation de signaux

Cependant, dans ce cas, l'erreur se produit uniquement pendant le test de l'unité.

Cela fonctionne bien dans les tests manuels, mais les tests unitaires échouent.

Y a-t-il quelque chose qui me manque?

Voici les extraits de code:

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)

Traceback:

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

----------------------------------------------------------------------
142
Gaurav Toshniwal

J'ai rencontré ce même problème moi-même. Cela est dû à une bizarrerie dans la façon dont les transactions sont gérées dans les versions plus récentes de Django, associée à un unittest qui déclenche intentionnellement une exception.

Un unestest vérifiait qu'une contrainte de colonne unique était appliquée en déclenchant intentionnellement une exception 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()

Dans Django 1.4, cela fonctionne bien. Cependant, dans Django 1.5/1.6, chaque test est encapsulé dans une transaction. Ainsi, si une exception se produit, elle est interrompue jusqu'à ce que vous l'annuliez explicitement. Par conséquent, toute opération ORM ultérieure dans cette transaction, telle que ma do_more_model_stuff(), échouera avec cette exception Django.db.transaction.TransactionManagementError.

Comme caio mentionné dans les commentaires, la solution consiste à capturer votre exception avec transaction.atomic comme:

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

Cela empêchera l’exception générée à dessein de rompre l’ensemble de la transaction de unittest.

185
Cerin

Puisque @mkoistinen n'a jamais fait son commentaire , une réponse, je posterai sa suggestion pour que les gens ne soient pas obligés de creuser avec des commentaires.

pensez simplement à déclarer votre classe de test en tant que TransactionTestCase plutôt que simplement TestCase.

A partir de docs : Un TransactionTestCase peut appeler commit et annulation et observer les effets de ces appels sur la base de données.

30
kdazzle

Pour moi, les correctifs proposés ne fonctionnaient pas. Dans mes tests, j'ouvre certains sous-processus avec Popen pour analyser/migrer les migrations (par exemple, un test vérifie s'il n'y a pas de modification de modèle).

Pour moi, le sous-classement de SimpleTestCase au lieu de TestCase a fait l'affaire.

Notez que SimpleTestCase ne permet pas d'utiliser la base de données.

Bien que cela ne réponde pas à la question initiale, j'espère que cela aidera quand même certaines personnes.

1
flix

J'ai le même problème, mais with transaction.atomic() et TransactionTestCase n'ont pas fonctionné pour moi.

python manage.py test -r au lieu de python manage.py test me convient, peut-être que l'ordre d'exécution est crucial

alors je trouve un doc sur Ordre dans lequel les tests sont exécutés , Il mentionne quel test sera exécuté en premier.

Donc, j'utilise TestCase pour l'interaction avec la base de données, unittest.TestCase pour un autre test simple, cela fonctionne maintenant!

0
Leo

Si vous utilisez pytest-Django, vous pouvez transmettre transaction=True au décorateur Django_db pour éviter cette erreur.

Voir https://pytest-Django.readthedocs.io/en/latest/database.html#testing-transactions

Django a lui-même le TransactionTestCase qui vous permet de tester transactions et va vider la base de données entre les tests pour isoler leur. L'inconvénient est que la configuration de ces tests est beaucoup plus lente en raison du vidage requis de la base de données. pytest-Django prend également en charge ce type de tests, que vous pouvez sélectionner à l'aide d'un argument de la marque Django_db:

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

La réponse de @kdazzle est correcte. Je n’ai pas essayé car les gens disaient que «la classe TestCase de Django est une sous-classe plus couramment utilisée de TransactionTestCase», j’ai donc pensé que c’était la même utilisation, l’un ou l’autre. Mais le blog de Jahongir Rahmonov l'expliquait mieux:

la classe TestCase encapsule les tests dans deux blocs atomic () imbriqués: un pour toute la classe et un pour chaque test. C'est ici que TransactionTestCase doit être utilisé. Il n’enveloppe pas les tests avec atomic () et ainsi vous pouvez tester vos méthodes spéciales qui nécessitent une transaction sans aucun problème.

EDIT: Cela n'a pas fonctionné, je pensais que oui, mais non.

En 4 ans, ils pourraient résoudre ce problème .......................................

0
Shil Nevado

Je recevais cette erreur lors de l'exécution de tests unitaires dans ma fonction create_test_data avec Django 1.9.7. Cela fonctionnait dans les versions précédentes de Django.

Cela ressemblait à ceci:

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

Ma solution consistait à utiliser update_or_create à la place:

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})
0
PhoebeB