Passing dynamic objects from setup to a test function in py.test
A comparison of global variables, fixture-result unpacking, and test-class instance attributes for sharing dynamically prepared objects between pytest setup and test execution.
Translated by ChatGPT GPT-6.1 Sol
Original publication details
Передача динамических объектов от setup к тестовой функции в py.test
Archived publication. Roles, figures, and technical instructions reflect the original publication date.
In large projects, at some point you reach a situation where there are already many tests, while your own high-level framework is being developed alongside them. In this case, the framework is a wrapper around the functions of the object under test and the capabilities of the various tools used on the project. On top of that, all the directories are full of fixtures, many of which are used in only one test file.
At this wonderful moment, certain problems arise. I have already written about one of them: implementing convenient parameterization, for example from a file. We will discuss the next, one of the most troublesome, in this article.
“Captain, we have lots of fixtures, and globals are appearing”
Overloading test directories with fixtures is a perfectly logical consequence of the concept built into py.test, but sometimes this approach goes beyond acceptable limits. We also often see constructs in tests that are meant to determine what information a test should use in a particular case, or an attempt to check whether the test can proceed while still preparing the environment.
The biggest misconception, when these constructs are passed from some setup fixture and run through the entire test, is to use global variables. I have encountered difficult cases like this, and that idea is one of the first to come to mind.
Here, it is worth mentioning that the fixture concept lets us avoid compromising the cleanliness of the code in this way, but also introduces additional layers of abstraction and many references. As a last resort, you could develop the terrible habit of unpacking a fixture’s result, but then we spoil the logs, since we have no separation into Setup, Run, and Teardown, and additionally complicate the code when unpacking results or create numerous shortcuts.
Let’s look at examples, starting with the terrible one:
“Fixtures and 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, "Мы увидели город которого нет"
As a result:
-
We have initial checks
-
We have the ill-fated
global
“Unpacking in all its glory”
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, "Мы увидели город которого нет"
As a result:
-
An unclear shortcut to the
setupfixture’s result -
Unpacking that conveys little information or can take a couple of lines, or even a dozen
While this may not look so critical in a small example, once a test grows to 400+ lines, it starts to stand out.
A little trick with classes
So what should we do when we have 8 fixtures in setup, from which we need to retrieve both several fixtures after using them to prepare the test and data we will process and validate outside the scope of our case?
That is a sign: it is time to take a class instance and pass everything through it. py.test will create the instance for us, so we only need to specify its attributes.
Let’s improve our example:
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, "Мы увидели город которого нет"
As a result:
-
Clean, clear passing of the objects we need
-
No unpacking or
global
When the method makes sense
Viewed as a complete example, this may seem unclear and entirely replaceable by other approaches. In this case, however, the example should be considered only as a demonstration of this possibility and a chance to judge the conciseness of the resulting code.
Essentially, we are talking strictly about dynamic objects for subsequent testing. This also applies to convenient shortcuts for later use in cases where the environment’s state does not depend on the input parameters and largely depends on external factors.
In my view, the most suitable use cases for the approach described are working with Android/iOS applications through Appium and testing IOT/Embedded devices.