pytest-dev / pytest-dev/pytest
Fixtures without scope
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 14.5k
- Forks
- 3.4k
- Avg merge
- 2d 9h
- Merged PRs (30d)
- 35
Description
It doesn't make much sense for something like a temporary directory to have the property of a “scope”. It should be up to the collection of tests to decide if their use case is such that the temporary directory can be reused.
Scoped fixtures make you structure your tests for those fixtures instead of logically. Changing fixture scope can mean a lot of refactoring.
So I think it would be nice to have fixtures without scopes.
Something like this might work well:
@fixture
def cat():
return "Tom the cat"
@cat.reuse
class CatTest:
"""here all tests use the same cat"""
This hopefully can be read as “CatTest tests can reuse cat”. The point here is to highlight that reusing fixtures is a mere optimization. It's not that you have to have the same instance of cat for each test, you don't; but just to speed things up, CatTest tests can reuse it. Perhaps @cat.can_be_reused?
For specific objects that can't be decorator targets, simply call:
cat.reuse(object)
cat.reuse("this") # refers to innermost enclosing scope, e.g. this module
To have session-reusable fixtures, you could put cat.reuse("this") in root conftest.py.
For multiple objects of the same kind, one could use
cat.reuse("every module")
cat.reuse("every class")
By putting cat.reuse("every class") in a module, every class in that module is marked to be able to reuse cat. This does not affect other modules, they are free to have their own rules of reusing fixtures.
To use the fixture for every test, even if it doesn't specifically depend on it:
cat.reuse("every test", auto=True) # or "every class", etc
Reusing fixtures in parametrized tests
It would be also nice to be able to reuse a fixture for every invocation of a test function of a parametrized test. I think this could be as simple as
@cat.reuse
@pytest.mark.parametrize(...)
def test_cat(cat):
...
Reuse scope conflicts
Consider a fixture that uses another one:
@fixture
def cat_owner(cat):
return f"Owner of {cat}"
@cat_owner.reuse
class CatOwnerTest:
def test_cat_owner(self, cat_owner):
"""here all tests use the same cat owner"""
As the cat doesn't have a scope, this poses no problem. The test doesn't use it, so the class can work with one copy of cat_owner and one copy of cat that it requires.
But what if the test used both cat_owner and cat?
@cat_owner.reuse
class CatOwnerTest:
def test_cat_owner(self, cat_owner, cat):
"""here all tests use the same cat owner... or not?"""
In vanilla pytest, this can only work if cat has the same or a wider scope than cat_owner. But with the idea that “reusing fixtures is a mere optimization” this doesn't have to be an error. I think this can be resolved in the following ways:
-
make
catreusable on the same or a wider scope, as you would do in regular pytest -
simply create a new
test_cat_owner(andcat) for each test.CatOwnerTestcan reusecat_owner, but it's only an optimization, so it's not really required! -
if this optimization is important, and you want to not accidentally forgo it, you could explicitly mark it as such, e.g.
@cat_owner.reuse(strict=True) class CatOwnerTest: def test_cat_owner(self, cat_owner, cat): """here all tests REALLY use the same cat owner"""in this situation,
cat_ownermust be reused, butcatis not marked as being reusable, so this will fail (unless it's the only test in the class?). -
use two
cats! one for the reusedcat_owner, and one for each test. Either makecatexclusive to the fixture:@cat.exclusive @fixture def cat_owner(cat): return f"Owner of {cat} who won't share it with anyone"or to the test:
@cat_owner.reuse class CatOwnerTest: @cat.exclusive def test_cat_owner(self, cat_owner, cat): """here all tests use the same cat owner... but different cats"""If the cat is a temporary directory, you now can have one for your fixture and one the test that's using it! Although, some fixtures can only exist as singularities (e.g. a web server), so an attempt to have two of them would be unwise. To fail fast, something like this could help:
@fixture(can_exist_at_most_one_at_a_time=True) def world(): return "Brave new world"
Fixture bleeding
This could be a separate issue. Consider this file:
@fixture
def foo():
...
@fixture(scope="session")
def bar():
...
def test_foo(foo):
...
def test_bar(bar):
...
The setup plan for this looks good ($pytest --setup-plan file.py):
SETUP F foo
file.py::test_foo (fixtures used: foo)
TEARDOWN F foo
SETUP S bar
file.py::test_bar (fixtures used: bar)
TEARDOWN S bar
But if you put test_foo below test_bar, the plan is a bit different:
SETUP S bar
file.py::test_bar (fixtures used: bar)
SETUP F foo
file.py::test_foo (fixtures used: foo)
TEARDOWN F foo
TEARDOWN S bar
Notice how in the second example, unlike in the first, during test_foo there exist two fixtures, not one, even though only one is explicitly or implicitly used by it.
In some cases, this is unacceptable. If you want to avoid this, you have to properly scope all tests that use this fixture, e.g. by putting them into a specific module. This can break the logical structure of tests. For this reason it might be useful to explicitly say that a fixture must not exist during tests that are not using it. Perhaps like this:
bar.reuse("this", allow_bleeding=False)
This might require this fixture to be torn down and set up again multiple times, but this would be OK as reusing fixtures is a mere optimization. This might be a property of the fixture itself, too.
I don't think it's possible to implement this as a plug-in. It is at this test def test_cat_owner(cat_owner) that it's decided which pytest scopes cat_owner and cat should have, and two different tests might want two different-scoped sets of these. If for a single fixture i can generate cat_class, cat_module etc, for a pair I'd have to create cat_owner_class_cat_module etc and it's just not feasible.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the two setup plans with pytest --setup-plan using the fixture examples in this issue, then trace pytest's fixture scope and dependency handling. A complete implementation would define the reuse and bleeding semantics, resolve scope conflicts, and add tests covering the proposed decorator and call forms.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100