pytest-dev / pytest-dev/pytest

Fixtures without scope

Open
#8,347 1 comment 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

topic: fixtures type: proposal
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 cat reusable on the same or a wider scope, as you would do in regular pytest

  • simply create a new test_cat_owner (and cat) for each test. CatOwnerTest can reuse cat_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_owner must be reused, but cat is not marked as being reusable, so this will fail (unless it's the only test in the class?).

  • use two cats! one for the reused cat_owner, and one for each test. Either make cat exclusive 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.