TransactionManagementError "Ви не можете виконувати запити до кінця" атомного "блоку під час використання сигналів, але лише під час тестування блоку.


195

Я отримую TransactionManagementError, коли намагаюся зберегти екземпляр моделі Django User і в його сигналі 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

testo.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, з іншого боку, не обрізає таблиці після тесту. Натомість він вкладає тестовий код у транзакцію бази даних, яка відкочується в кінці тесту. Обидва явні вчинки, такі як transak.com.commit () і неявні, які можуть бути викликані transak.atomic (), замінюються операцією nop. Це гарантує, що відкат в кінці тесту поверне базу даних до її початкового стану. "
Gaurav Toshniwal

6
Я знайшов свою проблему. Існував виняток IntegrityError, подібний цьому "спробуйте: ... крім IntegrityError: ...", що мені довелося зробити, це використовувати transak.atomic всередині пробного блоку: "try: withaction.atomic (): .. . крім IntegrityError: ... "зараз усе працює добре.
caio

docs.djangoproject.com/en/dev/topics/db/transaction, а потім шукайте "Обертання атомної спроби / за винятком блоку дозволяє природне поводження з помилками цілісності:"
CamHart

Відповіді:


238

Я сам зіткнувся з цією ж проблемою. Це спричинено химерністю того, як обробляються транзакції в новіших версіях Джанго в поєднанні з тестом одиниць, який навмисно запускає виняток.

У мене був блок-тест, який перевіряв, щоб переконатись у виконанні унікального обмеження стовпця, цілеспрямовано викликаючи виключення 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 у цій транзакції, як-от моя do_more_model_stuff(), не вдасться з цим django.db.transaction.TransactionManagementErrorвинятком.

Як і Кайо, згаданий у коментарях, рішення полягає в тому, щоб захопити ваше виняток 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

Це дозволить уникнути цілеспрямованого винятку, щоб зламати всю трансакцію єдиного тесту.


71
Також розгляньте, як просто оголосити свій тестовий клас як TransactionTestCase, а не просто TestCase.
mkoistinen

1
О, я знайшов відповідний документ з іншого питання . Документ тут .
yaobin

2
Для мене в мене вже був transaction.atomic()блок, але я отримав цю помилку і не мав поняття, чому. Я прийняв пораду цієї відповіді і поставив вкладений атомний блок всередині мого атомного блоку навколо зони неприємностей. Після цього вона дала детальну помилку помилки цілісності, яку я потрапила, що дозволило мені виправити код і зробити те, що я намагався зробити.
AlanSE

5
@mkoistinen TestCaseуспадковує, TransactionTestCaseтому не потрібно цього змінювати. Якщо ви не працюєте з БД під час тестового використання SimpleTestCase.
бнз

1
@bns Ви пропускаєте точку коментаря. Так, TestCaseуспадковується від, TransactionTestCaseале його поведінка зовсім інша: вона обробляє кожен метод тестування в транзакції. TransactionTestCaseз іншого боку, можливо, введено в оману: він обрізає таблиці, щоб скинути db - найменування, схоже, відображає те, що ви можете перевірити транзакції в рамках тесту, а не тест, який завершено як транзакцію!
CS

48

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

подумайте про те, що просто оголосити свій тестовий клас як TransactionTestCase, а не просто TestCase.

З Документів : TransactionTestCase може викликати фіксацію та відкат та спостерігати за наслідками цих дзвінків на базу даних.


2
+1 для цього, але, як кажуть документи, "клас тестування Django є частіше використовуваним підкласом TransactionTestCase". Щоб відповісти на початкове запитання, чи не слід використовувати SimpleTestCase замість TestCase? SimpleTestCase не має атомної бази даних.
daigorocub

@daigorocub При успадкуванні від SimpleTestCase, allow_database_queries = Trueпотрібно додати всередину тестового класу, щоб він не плював AssertionError("Database queries aren't allowed in SimpleTestCase...",).
CristiFati

Це відповідь, яка найкраще працює для мене, тому що я намагався перевірити на цілісність, буде піднято, а згодом мені потрібно було виконати більше запитів щодо збереження бази даних
Кім Стек

8

Якщо ви використовуєте pytest-django, ви можете перейти transaction=Trueдо django_dbдекоратора, щоб уникнути цієї помилки.

Дивіться https://pytest-django.readthedocs.io/en/latest/database.html#testing-transaction

Сам Джанго має TransactionTestCase, який дозволяє тестувати транзакції і перемикатиме базу даних між тестами, щоб їх ізолювати. Мінус цього полягає в тому, що ці тести встановлюються набагато повільніше через необхідну промивку бази даних. pytest-django також підтримує цей стиль тестів, який ви можете вибрати, використовуючи аргумент до позначки django_db:

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

У мене виникла проблема з цим рішенням, у мене були початкові дані в моєму БД (додані міграціями). Це рішення промиває базу даних, тому інші тести, залежні від цих початкових даних, почали виходити з ладу.
abumalick

1

Для мене запропоновані виправлення не спрацювали. У своїх тестах я відкриваю деякі підпроцеси Popenдля аналізу / тривалості міграцій (наприклад, один тест перевіряє, чи немає змін моделі).

Для мене підкласинг SimpleTestCaseзамість того, що TestCaseзробив свою справу.

Зверніть увагу, що SimpleTestCaseне дозволяє використовувати базу даних.

Хоча це не відповідає на початкове запитання, я сподіваюся, що це допоможе деяким людям.


1

Ось ще один спосіб зробити це на основі відповіді на це питання:

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

0

Я отримував цю помилку під час тестування одиниць у своїй функції create_test_data за допомогою django 1.9.7. Він працював у більш ранніх версіях джанго.

Виглядало так:

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()працює також, здається, що це .save (), він не любить функцію, оформлену транзакцією.atomic () (у мене не вдалося лише 1 дзвінок).
Тимофій Макобу

0

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

python manage.py test -rзамість python manage.py testмене нормально, можливо, порядок виконання має вирішальне значення

то я знаходжу документа про Порядок, в якому виконуються тести , в ньому згадується, який тест запуститься першим.

Отже, я використовую TestCase для взаємодії з базою даних, unittest.TestCaseдля іншого простого тесту, він працює зараз!


0

Відповідь @kdazzle правильна. Я не пробував, тому що люди сказали, що "клас тестування Django - це більш часто використовуваний підклас TransactionTestCase", тому я подумав, що це те саме використання того чи іншого. Але блог Ягонгіра Рахмонова пояснив це краще:

клас TestCase обертає тести у два вкладені атомні () блоки: один для всього класу та один для кожного тесту. Тут слід використовувати TransactionTestCase. Він не обертає тести блоком atomic (), і таким чином ви можете протестувати ваші спеціальні методи, які потребують транзакції без проблем.

EDIT: Це не спрацювало, я думав, що так, але НІ.

За 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)

Видалено цю помилку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.