#django #unit-testing #mocking #pytest #liveservertestcase
#django #модульное тестирование #издевательство #pytest #liveservertestcase
Вопрос:
Я использую 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
Все остальные тесты выполняются и проходят успешно, но все тесты в этом классе завершаются неудачей, потому что он не исправляет функцию.
Он пытается вызвать фактическую хранимую процедуру (которая завершается неудачей, потому что ее еще нет на рабочем сервере), и она завершается неудачей.
Почему он исправляется правильно, когда я специально вызываю этот тестовый набор, но не исправляется правильно, когда я просто запускаю pytest
на уровне приложения или проекта?
Я в растерянности и не уверен, как это хорошо отладить. Любая помощь приветствуется
Комментарии:
1. Вы уверены, что это причина?
@pytest.mark.usefixtures("standard_browser")
является атрибутом класса. Таким образом, он повторно используется всеми тестами. Может ли это вызвать те же симптомы? То же самое относится к setUp() без демонтажа (), который выполняет любую необходимую очистку.2. Тесты работают при индивидуальном запуске, но не при запуске на уровне приложения или проекта. Это приспособление просто открывает браузер и больше ничего не делает — закрывается по завершении теста, поэтому я не верю, что это имеет какое-либо влияние. Он работает при тестировании сам по себе. Кроме того, setUp () настраивает данные, которые автоматически усекаются между тестами Django, поэтому ничего не нужно очищать. Ошибка в журналах конкретно показывает, что он не запускает это приспособление, а вместо этого пытается запустить хранимую процедуру — поэтому исправление не применяется при запуске всего набора.
3. Импортируется ли этот метод из другого места при запуске полного набора? Причина
4. Я не знаю, если только pytest не делает что-то другое при запуске полного набора, а не только определенного класса тестов (в что я не верю); Я добавил служебную функцию и представление, которое вызывает эту функцию, к вопросу для более легкого понимания происходящего.
5. Ну, что это показывает мне, так это то, что есть 2 разных символа:
get_employee_sb7_data
иmy_app.utils.get_employee_sb7_data
. Они из одного и того же места, но с разными именами. Таким образом, разница будет заключаться в том, чтоviews.py
он импортируется раньшеtest_integrations.py
и каким-то образом все еще находится в области видимости, но при отдельном запуске сначала импортируется тестовый пример (что он и делает). Способ проверить теорию состоит в том, чтобы добавить views.py как импорт в тестовом примере, а затем оба должны демонстрировать одинаковое (не рабочее) поведение.
Ответ №1:
Итак, что происходит, так это то, что ваши представления импортируются до того, как вы выполняете исправление.
Давайте сначала посмотрим на рабочий случай:
- pytest импортирует файл test_integrations
- тест выполняется, и запускается внутренняя функция декоратора исправления
- пока нет импорта утилит, поэтому patch импортирует и заменяет функцию
- выполняется тело теста, которое передает URL-адрес тестовому клиенту
- тестовый клиент импортирует распознаватель и, в свою очередь, импортирует представления, которые импортируют утилиты.
- Поскольку утилиты уже исправлены, все работает нормально
Если сначала запускается другой тестовый пример, который также импортирует те же представления, тогда этот импорт выигрывает, и исправление не может заменить импорт.
Ваше решение — ссылаться на один и тот же символ. Итак, в test_integrations.py
:
@patch("myapp.views.get_employee_sb7_data")
Комментарии:
1. Я добавил импорт в верхней части файла (test_integrations . py) как вы и предлагали, затем изменил декоратор для функций этого тестового класса на be
@patch("get_employee_sb7_data")
, но получил ошибку:TypeError: Need a valid target to patch. You supplied: 'get_employee_sb7_data'
2. Мой плохой. Обновлено. Символ существует как абсолютный импорт, но как «имя», а не как цель.
3. Спасибо за отличное объяснение, а также за исправление того, что должно произойти. Теперь я лучше понимаю, что происходит (хотя макеты все еще немного сбивают меня с толку — документация кажется такой же ясной, как грязь! ha!). Я ценю это!