TransactionManagementError «Вы не можете выполнять запросы до конца« атомарного »блока» при использовании сигналов, но только во время модульного тестирования

140

Я получаю TransactionManagementError при попытке сохранить экземпляр модели пользователя Django и в своем сигнале 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 contact is the same as the user 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.

----------------------------------------------------------------------
  • 0
    Вы нашли проблему?
  • 0
    Еще нет. Тем не менее, я ожидаю, что это связано с тем, как транзакции управляются для самих тестовых случаев.
Показать ещё 6 комментариев
Теги:
unit-testing
django-signals

8 ответов

182
Лучший ответ

Я сам столкнулся с этой проблемой. Это связано с причудой в том, как обрабатываются транзакции в новых версиях Django в сочетании с unittest, который преднамеренно вызывает исключение.

У меня был unittest, который проверял, чтобы убедиться, что принудительное ограничение столбца было принудительно запущено с помощью исключения 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 каждый тест завершается транзакцией, поэтому, если возникает исключение, он прерывает транзакцию, пока вы явно не откажете ее обратно. Следовательно, любые дополнительные операции ORM в этой транзакции, такие как my 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

Это предотвратит исключение целенаправленного исключения из всей транзакции unittest.

  • 5
    with transaction.atomic(): добились цели, спасибо!
  • 53
    Также рассмотрите возможность объявления вашего тестового класса как TransactionTestCase, а не просто TestCase.
Показать ещё 6 комментариев
27

Поскольку @mkoistinen никогда не делал его комментарий, ответ, я выложу его предложение, чтобы людям не приходилось копать комментарии.

рассмотрите просто объявление вашего тестового класса как TransactionTestCase, а не только TestCase.

Из docs: TransactionTestCase может вызывать фиксацию и откат и наблюдать за эффектами этих вызовов в базе данных.

  • 2
    +1 за это, но, как говорят документы, «класс TestCase Джанго является более часто используемым подклассом TransactionTestCase». Чтобы ответить на первоначальный вопрос, не должны ли мы использовать SimpleTestCase вместо TestCase? SimpleTestCase не имеет элементарных функций базы данных.
  • 0
    @daigorocub При наследовании от SimpleTestCase необходимо добавить allow_database_queries = True в тестовый класс, чтобы он не allow_database_queries = True AssertionError("Database queries aren't allowed in SimpleTestCase...",) .
Показать ещё 1 комментарий
0

Если вы используете 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
0

Ответ @kdazzle правильный. Я не пробовал, потому что люди говорили, что класс Djangos TestCase является более часто используемым подклассом TransactionTestCase, поэтому я подумал, что это одно и то же использование. Но блог Джахонгира Рахмонова объяснил это лучше:

класс TestCase заключает тесты в два вложенных блока atomic(): один для всего класса и один для каждого теста. Это где TransactionTestCase должен быть использован. Он не оборачивает тесты блоком atomic(), и, таким образом, вы можете без особых проблем протестировать ваши специальные методы, которые требуют транзакции.

РЕДАКТИРОВАТЬ: Это не сработало, я думал, да, но нет.

Через 4 года они могли это исправить.......................................

0

Для меня предложенные исправления не сработали. В моих тестах я открываю некоторые подпроцессы с помощью Popen для анализа/миграции миграций (например, один тест проверяет, нет ли изменений в модели).

Для меня подклассы из SimpleTestCase вместо TestCase сделали TestCase дело.

Обратите внимание, что SimpleTestCase не позволяет использовать базу данных.

Хотя это не отвечает на первоначальный вопрос, я надеюсь, что это все равно поможет некоторым людям.

0

У меня такая же проблема, но with transaction.atomic() и TransactionTestCase не работают для меня.

python manage.py test -r вместо python manage.py test подходит для меня, возможно, порядок выполнения имеет решающее значение

тогда я найду документ о Заказ, в котором выполняются тесты, Он упоминает, какой тест будет выполняться первым.

Итак, я использую TestCase для взаимодействия с базой данных, unittest.TestCase для другого простого теста, теперь он работает!

0

Я получал эту ошибку при запуске модульных тестов в моей функции create_test_data с помощью django 1.9.7. Он работал в более ранних версиях django.

Это выглядело так:

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

Моим решением было вместо этого использовать update_or_create:

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})
  • 1
    get_or_create() работает, кажется, что это .save (), что ему не нравится внутри украшенной функции транзакции.atomic () (у меня произошел сбой только с одним вызовом).
-2

Я была такая же проблема.

В моем случае я делал это

author.tasks.add(tasks)

так что преобразовать его в

author.tasks.add(*tasks)

Убрал эту ошибку.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню