В больших проектах в какой-то момент получается ситуация, когда тестов на проекте уже много и параллельно развивается собственный высокоуровневый фреймворк. Фреймворк, в данном случае, как обертка над функциями объекта тестирования и возможностями различных инструментов которые используются на проекте. Кроме того все папки заполнены фикстурами, многие из которых используются только в одном тестовом файле.
В этот прекрасный момент возникают некоторые проблемы. Про одну из них я уже писал, это реализация удобной параметризации, например из файла. Про следующую, из наиболее злосчастных, поговорим в этой статье.
«Капитан, у нас много фикстур и появляются глобалы»
Перегрузка тестовых директорий фикстурами это вполне логичное следствие использования концепта который заложен в py.test, но иногда такой подход переходит за рамки приемлемого. Кроме того, мы часто можем наблюдать в тестах конструкции, которые призваны определить какую информацию надо взять тесту в отдельном определенном случае или желание изначально проверить возможность дальнейшего прохождения теста еще на стадии подготовки среды.
Самое большое заблуждение, в ситуации когда эти конструкции передаются из некой setup
фикстуры и идут вдоль всего теста, — использовать global
переменные. Я столкнулся с подобными тяжелыми случаями и эта идея приходит на ум одной из первых.
В этом месте стоит упомянуть, что концепция фикстур позволяет избежать такой порчи чистоты кода, но также даёт ещё дополнительные уровни абстракции и множество референсов. На крайний случай можно завести ужасную привычку распаковывать результат фикстуры, но в таком случае мы портим логи, т.к. у нас нет разделения на Setup, Run и Teardown, и дополнительно усложняем код в момент распаковки результатов или плодим множественные шорткаты.
Рассмотрим на примерах, и начнем с ужасного:
"Фикстуры и global"
import pytest
@pytest.fixture(autouse=True)
def setup(create_human, goto_room, goto_default_position, choose_window, get_current_view):
global human
global window
# Сделаем подготовительные действия
desired_room = 1 # Можем этот момент параметризировать, в зависимости от задачи
human = create_human("John", "Doe") # Допускаем что один и тот же человек во всех случаях
# Если что-то пошло не по плану, то дальше нет смысла продолжать
assert goto_room(human, desired_room), "{} не дошел в комнату {}".format(human.full_name, desired_room)
# Выбираем случайное окно
window = choose_window(desired_room)
view = get_current_view(window)
assert view, "В окне {} ничего нет".format (window)
yield
# В Teardown вернёмся в начальное место
goto_default_position(human)
@pytest.mark.parametrize(
"city, expected_result",
[
("New York", False),
("Berlin", False),
("Unknown", True)
]
)
def test_city_in_window(city, expected_result):
"""Проверим что за вид у нас за окном."""
window_view = human.look_into(window)
recognized_city = human.recognize_city(window_view)
assert (recognized_city == city) == expected_result, "Мы увидели город которого нет"
В результате:
- Есть первоначальные проверки
- Есть злосчастный
global
"Распаковка во всей красе"
import pytest
@pytest.fixture
def setup(create_human, goto_room, goto_default_position, choose_window, get_current_view):
# Сделаем подготовительные действия
desired_room = 1 # Можем этот момент параметризировать, в зависимости от задачи
human = create_human("John", "Doe") # Допускаем что один и тот же человек во всех случаях
# Если что-то пошло не по плану, то дальше нет смысла продолжать
assert goto_room(human, desired_room), "{} не дошел в комнату {}".format(human.full_name, desired_room)
# Выбираем случайное окно
window = choose_window(desired_room)
view = get_current_view(window)
assert view, "В окне {} ничего нет".format (window)
yield { "human": human, "window": window}
# В Teardown вернёмся в начальное место
goto_default_position(human)
@pytest.mark.parametrize(
"city, expected_result",
[
("New York", False),
("Berlin", False),
("Unknown", True)
]
)
def test_city_in_window(setup, city, expected_result):
"""Проверим что за вид у нас за окном."""
data = setup
window_view = data["human"].look_into(data["window"])
recognized_city = data["human"].recognize_city(window_view)
assert (recognized_city == city) == expected_result, "Мы увидели город которого нет"
В результате:
- Непонятный шорткат на результат фикстуры
setup
- Распаковка малоинформативная или может занять пару, а то и десяток, строк
Если на маленьком примере это не выглядит так критично, то когда тест разрастается до 400+ строк, это начинает бросаться в глаза.
Маленькая хитрость про классы
Так как же поступить, если мы видим перед собой 8 фикстур в setup
из которого нам надо достать: и несколько фикстур после использования для предустановок теста, и данные которые мы обработаем и провалидируем вне рамок нашего кейса?
Это звоночек — настало время взять инстанс класса и передать всё через него. Сам инстанс за нас создаст py.test, поэтому нам надо будет просто указать его атрибуты.
Усовершенствуем наш пример:
import pytest
class TestWindowView:
@pytest.fixture
def setup(self, create_human, goto_room, goto_default_position, choose_window, get_current_view):
# Сделаем подготовительные действия
desired_room = 1 # Можем этот момент параметризировать, в зависимости от задачи
self.human = create_human("John", "Doe") # Допускаем что один и тот же человек во всех случаях
# Если что-то пошло не по плану, то дальше нет смысла продолжать
assert goto_room(self.human, desired_room), "{} не дошел в комнату {}".format(human.full_name, desired_room)
# Выбираем случайное окно
self.window = choose_window(desired_room)
view = get_current_view(self.window)
assert view, "В окне {} ничего нет".format (self.window)
yield
# В Teardown вернёмся в начальное место
goto_default_position(self.human)
@pytest.mark.parametrize(
"city, expected_result",
[
("New York", False),
("Berlin", False),
("Unknown", True)
]
)
def test_city_in_window(self, setup, city, expected_result):
"""Проверим что за вид у нас за окном."""
window_view = self.human.look_into(self.window)
recognized_city = self.human.recognize_city(window_view)
assert (recognized_city == city) == expected_result, "Мы увидели город которого нет"
В результате:
- Чистая и внятная передача нужных объектов
- Никаких распаковок и
global
Про целесообразность метода
С точки зрения целостности взятый пример кажется невнятными и полноценно заменяемым другими подходами, но в данном случае этот пример стоит учитывать только как демонстрацию подобной возможности и возможность оценить лаконичность итогового кода.
По сути же, речь идёт сугубо о динамических объектах для последующего тестирования. Это также касается удобных шорткатов для дальнейшего использования в тех случаях, когда состояние среды не зависит от входных параметров и во многом зависит от внешних факторов.
На мой взгляд наиболее подходящими сюжетами использования описанного подхода является работа с Android/iOS приложениями через Appium и тестирование IOT/Embedded устройств.