pytest-dev / pytest

The pytest framework makes it easy to write small tests, yet scales to support complex functional testing
https://pytest.org
MIT License
12.14k stars 2.69k forks source link

extract or integrate a dependency injection mechanism for fixtures #2829

Open RonnyPfannschmidt opened 7 years ago

RonnyPfannschmidt commented 7 years ago

CC @cjw296

the current fixture mechanism is something that grew entirely inside of pytest and is not usable without it which in turn creates the need for parallel infrastructure when wanting to use the actual things again

cjw296 commented 7 years ago

Okay, so just wanted to link to my previous comment here: https://github.com/pytest-dev/pytest/issues/2828#issuecomment-335732306

@RonnyPfannschmidt mentioned:

Can you walk me through each of those, perhaps with some pseudocode that would be what you would wish the code inside pytest to look like?

RonnyPfannschmidt commented 7 years ago

in pytest fixures belong to a scope and follow a parameterset - they get torn down/set up between tests as needed

so we need setup, teardown and controll of lifetime as we run each test item its hard to come up with pseudo code as the requirements are not clear yet

cjw296 commented 7 years ago

Sure, scopes seem as simple as:

new_context = context.push()
...
old_context = context.pop()

...or just ditch references to new_context.

Teardown: yeah, that is a hard one, especially with something like:

@pytest.fixture()
def some_fixture():
    # setup
    yield something
    # teardown
RonnyPfannschmidt commented 7 years ago

@cjw296 scopes in pytest are also bound to fixtures, so at function scope if a session scope fixture is requested, it is made at session scope and kept there

cjw296 commented 7 years ago

Sure, so we'd need a mapping of contexts:

context = Context()
scopes = {}
for scope in 'session', 'module', 'class', ...:
    scopes[scope] = Scope(context.push())

...with the most specific keys being discarded as each class/function/etc was processed.

So, thinking more about the setup/teardown problem, assuming we have a pytest Item that has the callable as .obj:

# during discovery
requires, returns = extract_declarations(item.obj)
scope = Scope()
scope.add_requirements(requires)
# then something something as above with the nested contexts

The same code could be used to figure out what each fixture returns. I'm still not 100% about parameterisation?

Thoughts?

RonnyPfannschmidt commented 7 years ago

@cjw296 i believe its necessary to first actually write down the feature set of pytest fixtures,

as every iteration i notice a a new hole in the examples you generate (in particular since its easy to find the holes and hard to make up a shape of an api that doesn't have them, so i get the consistently easy side of this one and you get the increasingly harder side of this one)

(current example dont take into account scope affinities of fixtures and wrapping fixtures of with fixtures of the same name)

i believe we need to approach this differently in other to work it out to satisfaction (i dont think any of us is happy with the current path of the conversation thats dwelving into details without producing something working)

cjw296 commented 7 years ago

Sure, what might make sense is for pytest to refactor and try and meet in the middle.

FWIW, the scope affinities of fixtures with the same name is actually taken care of with the nested scopes suggestions I've made, but I'm sure there are more complications.

That said, I'd be very happy to see Mush form part of the solution, so please me know if there are enhancements such as the nested contexts stuff I've listed above, that would make it's use easier.

I'm fairly confident that the requirements discovery and resource definition stuff in Mush is already flexible enough for pytest's needs and is certainly robust. The challenges will come, as you point out, in the fixture lifecycle stuff, but that doesn't seem outrageous ;-)