Description
What's the problem this feature will solve?
We in userver like to create test libraries with mocks and utilities packed in pytest plugins.
In a service's root conftest.py
we list the used plugins with pytest_plugins
variable, then throw in some service-wide fixtures. It is also natural to put overrides of global fixtures there (where needed), this works as expected, in the spirit that a root conftest works as the latest plugin.
We've added a feature using pytest_plugin_registered
that allows any plugin to provide a piece of JSON config by putting USERVER_CONFIG_PART
global variable in the plugin. Our config
plugin essentially walks through all the plugins using pytest_plugin_registered
and merges their JSON patches, allowing the later plugins to override the config parts of the earlier ones.
One issue we've encountered is, suddenly conftest.py
does not have priority over other plugins! Example:
# foo_lib_plugin.py
USERVER_CONFIG_PART = {
'should-frobnicate': False,
}
# ...
# root conftest.py
pytest_plugins = [
'config',
'foo_lib_plugin',
'bar_lib_plugin',
]
USERVER_CONFIG_PART = {
'should-frobnicate': True,
}
In the end, the merged config looks like:
{
"should-frobnicate": false
}
This is highly unexpected and has cost me a whole day of head-scratching.
Why is conftest.py
not applied as "the latest plugin" here?
One possible fix in this case is to extract all the overrides into a separate plugin:
# foo_lib_plugin.py
USERVER_CONFIG_PART = {
'should-frobnicate': False,
}
# ...
# my_service_plugin.py
USERVER_CONFIG_PART = {
'should-frobnicate': True,
}
# root conftest.py
pytest_plugins = [
'config',
'foo_lib_plugin',
'bar_lib_plugin',
'my_service_plugin',
]
Now, thinking more generally, I can understand why this is the way it is. A test session can have 1 or more "root" conftest.py
files (in the root test directory or higher), all of them are treated as fully-fledged plugins. As a plugin, a conftest.py
can load other plugins using pytest_plugins
or pytest_configure
or in some other ways. In general, we'll only know the whole list of plugin dependencies after the plugin has been applied. So for consistency, let's register the conftest plugin completely, get, in particular, the list of requested plugins, then register them.
This is different to how our team thinks of conftest.py
. We essentially think of the root conftest as playing two roles at the same time:
- The "mother of all plugins", contains the list of all plugins needed for the service, we do not use other plugin sources.
- The latest, most specific plugin that can be used for service-specific fixtures and overrides.
Unfortunately, these roles do not live along under the current pytest rules, as the plugin ordering becomes inconsistent according to this mental model.
Describe the solution you'd like
This quirk can at least be documented. While loading stages are somewhat well described, plugin ordering guarantees w.r.t. pytest_plugin_registered
can be documented more thoroughly, especially with respect to pytest_plugins
and conftest.py
.
Still, despite not being a bug, I believe this is an inconsistency issue. A conftest.py
can be thought of as "the last plugin" in the context of fixtures, but a deep dive breaks this simple mental model.
I don't have a good solution at hand, but would be happy to hear one.