TransactionManagementError “No puede ejecutar consultas hasta el final del bloque 'atómico'” mientras usa señales, pero solo durante las Pruebas unitarias

194

Recibo TransactionManagementError cuando intento guardar una instancia de modelo de usuario de Django y, en su señal post_save, guardo algunos modelos que tienen al usuario como clave foránea.

El contexto y el error es bastante similar a esta pregunta django TransactionManagementError cuando se usan señales

Sin embargo, en este caso, el error ocurre solo durante la prueba de la unidad .

Funciona bien en las pruebas manuales, pero las pruebas unitarias fallan.

¿Hay algo que me falta?

Aquí están los fragmentos de código:

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)

Rastrear:

======================================================================
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
fuente
De los documentos: "Un TestCase, por otro lado, no trunca las tablas después de una prueba. En cambio, encierra el código de prueba en una transacción de base de datos que se revierte al final de la prueba. Ambas confirmaciones explícitas como transacción.commitir () y los implícitos que pueden ser causados ​​por transacción.atomic () se reemplazan con una operación nop. Esto garantiza que la reversión al final de la prueba restaura la base de datos a su estado inicial ".
Gaurav Toshniwal
66
Encontré mi problema. Hubo una excepción de IntegrityError como esta "try: ... except IntegrityError: ..." lo que tuve que hacer es usar la transacción.atomic dentro del bloque try: "try: with transaction.atomic (): .. excepto IntegrityError: ... "ahora todo funciona bien.
caio
docs.djangoproject.com/en/dev/topics/db/transactions y luego busque "Envolver atomic en un bloque try / except permite el manejo natural de los errores de integridad:"
CamHart

Respuestas:

236

Me encontré con este mismo problema yo mismo. Esto se debe a una peculiaridad en la forma en que se manejan las transacciones en las versiones más recientes de Django junto con una prueba unitaria que desencadena intencionalmente una excepción.

Tuve una prueba de unidad que verificó para asegurarme de que se aplicara una restricción de columna única al activar intencionalmente una excepción 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()

En Django 1.4, esto funciona bien. Sin embargo, en Django 1.5 / 1.6, cada prueba se envuelve en una transacción, por lo que si se produce una excepción, se rompe la transacción hasta que la deshaga explícitamente. Por lo tanto, cualquier otra operación ORM en esa transacción, como my do_more_model_stuff(), fallará con esa django.db.transaction.TransactionManagementErrorexcepción.

Como caio mencionado en los comentarios, la solución es capturar su excepción con me transaction.atomicgusta:

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

Eso evitará que la excepción lanzada a propósito rompa toda la transacción de unittest.

Cerin
fuente
70
Considere también declarar su clase de prueba como TransactionTestCase en lugar de solo TestCase.
mkoistinen
1
Oh, encontré el documento relacionado de otra pregunta . El documento esta aquí .
yaobin 01 de
2
Para mí, ya tenía un transaction.atomic()bloqueo, pero recibí este error y no tenía idea de por qué. Tomé el consejo de esta respuesta y puse un bloque atómico anidado dentro de mi bloque atómico alrededor del área problemática. Después de eso, dio un error detallado del error de integridad que golpeé, lo que me permitió corregir mi código y hacer lo que estaba tratando de hacer.
AlanSE
55
@mkoistinen TestCaseestá heredando, TransactionTestCaseasí que no hay necesidad de cambiar eso. Si no opera en DB en uso de prueba SimpleTestCase.
bns
1
@bns te estás perdiendo el punto del comentario. Sí TestCasehereda de, TransactionTestCasepero su comportamiento es bastante diferente: envuelve cada método de prueba en una transacción. TransactionTestCase, por otro lado, quizás se denomina de manera engañosa: trunca las tablas para restablecer la base de datos; la denominación parece reflejar que puede probar transacciones dentro de una prueba, ¡no que la prueba esté envuelta como una transacción!
CS
48

Dado que @mkoistinen nunca hizo su comentario , una respuesta, publicaré su sugerencia para que la gente no tenga que buscar los comentarios.

considere simplemente declarar su clase de prueba como TransactionTestCase en lugar de solo TestCase.

De los documentos : Un TransactionTestCase puede llamar a commit y rollback y observar los efectos de estas llamadas en la base de datos.

kdazzle
fuente
2
+1 para esto, pero, como dicen los documentos, "la clase TestCase de Django es una subclase más utilizada de TransactionTestCase". Para responder a la pregunta original, ¿no deberíamos usar SimpleTestCase en lugar de TestCase? SimpleTestCase no tiene las características de la base de datos atómica.
daigorocub
@daigorocub Al heredar de SimpleTestCase, allow_database_queries = Truedebe agregarse dentro de la clase de prueba, para que no escupe un AssertionError("Database queries aren't allowed in SimpleTestCase...",).
CristiFati
Esta es la respuesta que mejor funciona para mí, ya que estaba tratando de probar la integridad del error y luego tuve que ejecutar más consultas de guardado de la base de datos
Kim Stacks
8

Si usa pytest-django, puede pasar transaction=Trueal django_dbdecorador para evitar este error.

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

Django tiene el TransactionTestCase que le permite probar transacciones y vaciará la base de datos entre pruebas para aislarlas. La desventaja de esto es que estas pruebas son mucho más lentas de configurar debido al vaciado requerido de la base de datos. pytest-django también admite este estilo de pruebas, que puede seleccionar utilizando un argumento para la marca django_db:

@pytest.mark.django_db(transaction=True)
def test_spam():
    pass  # test relying on transactions
frmdstryr
fuente
Tuve un problema con esta solución, tenía datos iniciales en mi base de datos (agregado por migraciones). Esta solución elimina la base de datos, por lo que otras pruebas que dependen de estos datos iniciales comenzaron a fallar.
abumalick
1

Para mí, las correcciones propuestas no funcionaron. En mis pruebas, abro algunos subprocesos conPopen para analizar / migrar pelusas (por ejemplo, una prueba verifica si no hay cambios en el modelo).

Para mí, la subclase de en SimpleTestCaselugar de TestCasehacer el truco.

Tenga en cuenta que SimpleTestCaseno permite usar la base de datos.

Si bien esto no responde a la pregunta original, espero que esto ayude a algunas personas de todos modos.

flix
fuente
1

Aquí hay otra forma de hacerlo, basada en la respuesta a esta pregunta:

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

Recibí este error al ejecutar pruebas unitarias en mi función create_test_data usando django 1.9.7. Funcionó en versiones anteriores de django.

Se veía así:

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

Mi solución fue usar update_or_create en su lugar:

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
fuente
1
get_or_create()funciona bien, parece que es el .save () que no le gusta dentro de una función decorada de la transacción.atomic () (la mía falló con solo 1 llamada allí).
Timothy Makobu
0

Tengo el mismo problema, pero with transaction.atomic()y TransactionTestCaseno funcionó para mí.

python manage.py test -ren lugar de python manage.py testestá bien para mí, tal vez el orden de ejecución es crucial

luego encuentro un documento sobre Order en el que se ejecutan las pruebas . Menciona qué prueba se ejecutará primero.

Entonces, uso TestCase para la interacción de la base de datos, unittest.TestCasepara otra prueba simple, ¡funciona ahora!

León
fuente
0

La respuesta de @kdazzle es correcta. No lo intenté porque la gente decía que 'la clase TestCase de Django es una subclase más utilizada de TransactionTestCase', así que pensé que era el mismo uso uno u otro. Pero el blog de Jahongir Rahmonov lo explicó mejor:

la clase TestCase envuelve las pruebas dentro de dos bloques atómicos () anidados: uno para toda la clase y otro para cada prueba. Aquí es donde se debe usar TransactionTestCase. No envuelve las pruebas con el bloque atomic () y, por lo tanto, puede probar sus métodos especiales que requieren una transacción sin ningún problema.

EDITAR: No funcionó, pensé que sí, pero NO.

En 4 años podrían arreglar esto .......................................

Shil Nevado
fuente
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
fuente
-4

Tuve el mismo problema.

En mi caso estaba haciendo esto

author.tasks.add(tasks)

convirtiéndolo a

author.tasks.add(*tasks)

Eliminado ese error.

Diaa Mohamed Kasem
fuente