Я использую Django и Pytest специально для запуска набора тестов и пытаюсь проверить, отображается ли конкретная форма с ожидаемыми данными, когда пользователь заходит на сайт (интеграционный тест).
В этом конкретном представлении используется хранимая процедура, над которой я издеваюсь, поскольку тест никогда не будет иметь к ней доступа.
Мой тестовый код выглядит так:
#test_integrations.py
from my_app.tests.data_setup import setup_data, setup_sb7_data
from unittest.mock import patch
...
# Setup to use a non-headless browser so we can see whats happening for debugging
@pytest.mark.usefixtures("standard_browser")
class SeniorPageTestCase(StaticLiveServerTestCase):
"""
These tests surround the senior form
"""
@classmethod
def setUpClass(cls):
cls.host = socket.gethostbyname(socket.gethostname())
super(SeniorPageTestCase, cls).setUpClass()
def setUp(self):
# setup the dummy data - this works fine
basic_setup(self)
# setup the 'results'
self.sb7_mock_data = setup_sb7_data(self)
@patch("my_app.utils.get_employee_sb7_data")
def test_senior_form_displays(self, mock_sb7_get):
# login the dummy user we created
login_user(self, "futureuser")
# setup the results
mock_sb7_get.return_value = self.sb7_mock_data
# hit the page for the form
self.browser.get(self.live_server_url + "/my_app/senior")
form_id = "SeniorForm"
# assert that the form displays on the page
self.assertTrue(self.browser.find_element_by_id(form_id))
# utils.py
from django.conf import settings
from django.db import connections
def get_employee_sb7_data(db_name, user_number, window):
"""
Executes the stored procedure for getting employee data
Args:
user_number: Takes the user_number
db (db connection): Takes a string of the DB to connect to
Returns:
"""
cursor = connections[db_name].cursor()
cursor.execute(
'exec sp_sb7 %s, "%s"' % (user_number, window.senior_close)
)
columns = [col[0] for col in cursor.description]
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
return results
# views.py
from myapp.utils import (
get_employee_sb7_data,
)
...
###### Senior ######
@login_required
@group_required("user_senior")
def senior(request):
# Additional Logic / Getting Other Models here
# Execute stored procedure to get data for user
user_number = request.user.user_no
results = get_employee_sb7_data("production_db", user_number, window)
if not results:
return render(request, "users/senior_not_required.html")
# Additional view stuff
return render(
request,
"users/senior.html",
{
"data": data,
"form": form,
"results": results,
},
)
Если я запущу этот тест с помощью:
pytest my_app/tests/test_integrations.py::SeniorPageTestCase
Тесты проходят без проблем. Появляется браузер - появляется форма с фиктивными данными, как мы и ожидали, и все работает.
Однако, если я запускаю:
pytest my_app
Все остальные тесты выполняются и проходят успешно, но все тесты в этом классе завершаются неудачно, потому что они не исправляют функцию.
Он пытается вызвать настоящую хранимую процедуру (что не удается, потому что ее еще нет на рабочем сервере) и терпит неудачу.
Почему он исправляет правильно, когда я специально вызываю этот TestCase, но не исправляет правильно, когда я просто запускаю pytest
на уровне приложения или проекта?
Я в недоумении и не уверен, как это очень хорошо отлаживать. Любая помощь приветствуется
Тесты работают при индивидуальном запуске, но не при запуске на уровне приложения или проекта. Это приспособление просто открывает браузер и больше ничего не делает — закрывается по завершении теста — поэтому я не верю, что это имеет какое-либо влияние. Он работает, когда тестируется сам по себе. Кроме того, setUp() настраивает данные, которые Django автоматически усекает между тестами, поэтому нечего очищать. Ошибка в журналах конкретно показывает, что он не запускает это приспособление и вместо этого пытается запустить хранимую процедуру, поэтому исправление не применяется при запуске всего набора.
Этот метод импортируется из другого места при запуске полного набора? Причина
Не то, чтобы я знал, если только pytest не делает что-то другое при запуске полного набора по сравнению с определенным классом тестов (что я не верю, что это так); Я добавил служебную функцию и представление, которое вызывает эту функцию, к вопросу для облегчения понимания того, что происходит.
Что ж, это показывает мне, что есть 2 разных символа: get_employee_sb7_data
и my_app.utils.get_employee_sb7_data
. Они из одного места, но с разными именами. Таким образом, разница будет заключаться в том, что views.py
импортируется раньше test_integrations.py
и каким-то образом все еще находится в области видимости, но при отдельном запуске он сначала импортирует тестовый пример (что он и делает). Способ проверить теорию состоит в том, чтобы добавить views.py в качестве импорта в тестовом случае, и тогда оба должны демонстрировать одинаковое (не работающее) поведение.
Вы правы, если я добавлю import views.py
в свой файл test_integrations.py
- та же ошибка возникает, когда я запускаю конкретно этот тестовый класс (а также когда я запускаю весь набор). Я издеваюсь/исправляю это совершенно неправильно в этом случае?
Итак, что происходит, так это то, что ваши представления импортируются до того, как вы вносите исправления.
Давайте сначала посмотрим рабочий случай:
Если сначала запускается другой тестовый пример, который также импортирует те же представления, то этот импорт выигрывает, и исправление не может заменить импорт.
Ваше решение состоит в том, чтобы ссылаться на один и тот же символ. Итак, в test_integrations.py
:
@patch("myapp.views.get_employee_sb7_data")
Я добавил импорт вверху файла (test_integrations.py), как вы предложили, затем изменил декоратор функций этого тестового класса на @patch("get_employee_sb7_data")
, но получил ошибку: TypeError: Need a valid target to patch. You supplied: 'get_employee_sb7_data'
Виноват. Обновлено. Символ существует как абсолютный импорт, но как «имя», а не цель.
Спасибо за отличное объяснение, а также исправление того, что должно произойти. Теперь я лучше понимаю, что происходит (хотя Mocks все еще немного сбивает меня с толку — документация кажется такой же ясной, как грязь! Ха!). Я ценю это!
Вы уверены, что это причина?
@pytest.mark.usefixtures("standard_browser")
— это атрибут класса. Таким образом, он повторно используется всеми тестами. Может ли это вызвать такие же симптомы? То же самое относится и к setUp() без tearDown(), выполняющего всю необходимую очистку.