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
11.98k stars 2.66k forks source link

conftest.py definitions in subdirectories shadow those in parent directories #10851

Open madhadron opened 1 year ago

madhadron commented 1 year ago

When there is a conftest.py in a subdirectory that contains a pytest.fixture of the same name as one in a conftest.py file in a parent directory, the fixture in the subdirectory runs for tests in the subdirectory, but the one in the parent does not.

Repro

Create a directory. Put in:

conftest.py

conftest.py

import os
import pytest

@pytest.fixture(autouse=True)
def record_file_attribute(record_property, request):
    file_path = os.path.relpath(request.node.location[0])
    record_property("file", file_path)

test_hello.py

def test_hello(record_property):
    assert True

submod/__init__.py (empty file)

submod/test_hello.py with the same contents as test_hello.py above.

submod/conftest.py with the same contents as test_hello.py above, but with "file" changed to "file2".

Run pytest in the root directory with --junitxml=output.xml. The output will be:

<?xml version="1.0" encoding="utf-8"?>
<testsuites>
  <testsuite name="pytest" errors="0" failures="0" skipped="0" tests="2" time="0.028" timestamp="2023-03-30T11:32:48.799625" hostname="fross-mbp">
    <testcase classname="test_minimal" name="test_hello" file="test_hello.py" line="0" time="0.001">
      <properties>
        <property name="file" value="test_hello.py"/>
      </properties>
    </testcase>
    <testcase classname="submod.test_hello" name="test_other" file="submod/test_hello.py" line="0" time="0.001">
      <properties>
        <property name="file2" value="submod/test_hello.py"/>
      </properties>
    </testcase>
  </testsuite>
</testsuites>

instead of having a file property on both.

pytest version 6.2.5

RonnyPfannschmidt commented 1 year ago

That is intentional

If you want the parent fixture to run, take it as a parameter

madhadron commented 1 year ago

Should you at least provide a warning that it is being shadowed?

RonnyPfannschmidt commented 1 year ago

Introduction of a marker to indicate whether a fixture should be overridden seems like a good ida, there is a number of cases where its intentional

The-Compiler commented 1 year ago

If you want the parent fixture to run, take it as a parameter

(you accidentally posted twice, I deleted the redundant comment)

Looks like even that doesn't always work. I didn't try with conftest.py files, but if you have two plugins, depending on plugin order, the "overriding" fixture will actually be loaded first, and then be shadowed by the to-be-overridden one. While the overriding one has an (unresolved) dependency on the to-be-overridden one, it's never actually called, so there is no failure.

Here is a reproducer:

import pytest

pytest_plugins = ["pytester"]

class Plugin1:
    @pytest.fixture
    def fixt(self):
        pass

class Plugin2:
    @pytest.fixture
    def fixt(self, fixt):
        1/0  # never gets called

def test_override(pytester):
    pytester.makepyfile("""
    def test_fixt(fixt):
        pass
    """)
    res = pytester.runpytest("--setup-show", plugins=[Plugin2(), Plugin1()])   # change plugin order to make override work
    print(res.stdout)
    assert False

While here it's trivial to change the plugin order, for conftest.py files (or installed plugins), that's not going to work.

The-Compiler commented 3 months ago

I had a few discussions here with @lovetheguitar about this, but we didn't really get anywhere. The gist of it is that overriding fixtures is both done accidentally (as in the OP or my comment above), as well as intentionally (e.g. many plugins let you override a fixture in your conftest.py to configure some aspect of it).

I like the idea that @RonnyPfannschmidt had mentioned above, and would imagine that as a @pytest.fixture(override=True), with pytest emitting a warning if an override happens without override=True being given.

Open questions:

RonnyPfannschmidt commented 3 months ago

this has some special edge cases - some fixutres are intended to be replaced (like the webdriver ones for selenium, others are not, but may or may not end up overtwritten depending on known plugins