Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
How to modularize your py.test fixtures

Using py.test is great and the support for test fixtures is pretty awesome. However, in order to share your fixtures across your entire module, py.test suggests you define all your fixtures within one single conftest.py file. This is impractical if you have a large quantity of fixtures -- for better organization and readibility, you would much rather define your fixtures across multiple, well-named files. But how do you do that? ...No one on the internet seemed to know.

Turns out, however, you can define fixtures in individual files like this:

tests/fixtures/add.py

import pytest

@pytest.fixture
def add(x, y):
    x + y

Then you can import these fixtures in your conftest.py:

tests/conftest.py

import pytest
from fixtures.add import add

...and then you're good to test!

tests/adding_test.py

import pytest

@pytest.mark.usefixtures("add")
def test_adding(add):
    assert add(2, 3) == 5

Because of the modularity, tests will have to be run with python -m py.test instead of py.test directly.

@ghost

This comment has been minimized.

Copy link

@ghost ghost commented Sep 21, 2017

thanks

@AndrewBMartin

This comment has been minimized.

Copy link

@AndrewBMartin AndrewBMartin commented Oct 17, 2017

I'm having trouble making this work when running multiple tests at the same time. Would appreciate any insight you might have. Please see the SO question.

Thank you

@boxysean

This comment has been minimized.

Copy link

@boxysean boxysean commented Oct 26, 2017

thank you

@ikonst

This comment has been minimized.

Copy link

@ikonst ikonst commented Mar 6, 2018

You can simply create "local pytest plugins" which can be nothing more than Python files with fixtures, e.g.

  • tests/unit/conftest.py:

    pytest_plugins = [
       "tests.unit.fixtures.some_stuff",
    ]
  • tests/unit/fixtures/some_stuff.py:

    import pytest
    
    @pytest.fixture
    def foo():
        return 'foobar'
@ryanjdillon

This comment has been minimized.

Copy link

@ryanjdillon ryanjdillon commented Oct 11, 2018

Awesome ikonst!

As a note that the respective directories referred to in tests.unit.fixtures.some_stuff" need to have __init__.py files for the plugins to be loaded by pytest.

https://docs.pytest.org/en/documentation-restructure/how-to/writing_plugins.html#requiring-loading-plugins-in-a-test-module-or-conftest-file

@kown7

This comment has been minimized.

Copy link

@kown7 kown7 commented Apr 29, 2019

Fixtures with a

@pytest.fixture(scope="session", autouse=True)

scope need to be in the conftest.py file directly.

@jpadhye

This comment has been minimized.

Copy link

@jpadhye jpadhye commented May 8, 2019

With python3 you run it as python -m pytest

Otherwise you see following error:


$ python -m py.test
/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/lib/python3.7/runpy.py:125: RuntimeWarning: 'py.test' found in sys.modules after import of package 'py', but prior to execution of 'py.test'; this may result in unpredictable behaviour
  warn(RuntimeWarning(msg))
/Users/jpadhye/.local/share/virtualenvs/sse-test-bench-4eGiHs-X/bin/python: loader for pytest cannot handle py.test
@Seneketh

This comment has been minimized.

Copy link

@Seneketh Seneketh commented Jan 9, 2020

Thanks for sharing!

@putnamhill

This comment has been minimized.

Copy link

@putnamhill putnamhill commented Apr 20, 2020

Oh thank you! Just what I need at the moment.

@ramast

This comment has been minimized.

Copy link

@ramast ramast commented May 6, 2020

You can simply create "local pytest plugins" which can be nothing more than Python files with fixtures, e.g.

* tests/unit/conftest.py:
  ```python
  pytest_plugins = [
     "tests.unit.fixtures.some_stuff",
  ]
  ```

* tests/unit/fixtures/some_stuff.py:
  ```python
  import pytest
  
  @pytest.fixture
  def foo():
      return 'foobar'
  ```

I really like your idea!

I've replaced

       pytest_plugins = [
          "tests.unit.fixtures.some_stuff",
       ]

with

from glob import glob

pytest_plugins = [
   fixture.replace("/", ".").replace("fixtures.py", "fixtures") for fixture in glob("*/tests/fixtures.py")
]

Then any fixture located in */tests/fixtures.py will be added automatically without extra work from me. You can adjust the pattern to fit your application's structure.

@christian-steinmeyer

This comment has been minimized.

Copy link

@christian-steinmeyer christian-steinmeyer commented Sep 14, 2020

In case, someone else likes the combination of both ideas by @ramast and @ikonst, I've adapted it to look up all fixtures, defined in their own file under the fixtures folder:

# conftest.py

from glob import glob


def refactor(string: str) -> str:
    return string.replace("/", ".").replace("\\", ".").replace(".py", "")


pytest_plugins = [
    refactor(fixture) for fixture in glob("tests/fixtures/*.py") if "__" not in fixture
]

So, this should work, if you project setup looks like this:

./
├── src
├── tests
│   ├── conftest.py
│   ├── fixtures
│   │   ├── my_fixture.py

edit: I noticed, that I was executing pytest from tests instead of root. Fixed the discovery path and shortened string manipulation.

@ikonst

This comment has been minimized.

Copy link

@ikonst ikonst commented Sep 14, 2020

I do something similar in my projects.

  • walk_packages.py:

    from pkgutil import ModuleInfo
    from pkgutil import walk_packages
    from types import ModuleType
    from typing import Iterable
    
    
    def get_packages_in_module(m: ModuleType) -> Iterable[ModuleInfo]:
        return walk_packages(m.__path__, prefix=m.__name__ + '.')  # type: ignore
    
    
    def get_package_paths_in_module(m: ModuleType) -> Iterable[str]:
        return [package.name for package in get_packages_in_module(m)]
  • conftest.py

    import tests.fixtures
    from tests.helpers.walk_packages import get_package_paths_in_module
    
    pytest_plugins = [
        ...
        *get_package_paths_in_module(tests.fixtures),
    ]

Admittedly the glob-and-string-substitution variant is a little shorter :)

@dgaikwad

This comment has been minimized.

Copy link

@dgaikwad dgaikwad commented Oct 14, 2020

@peterhurford and @ikonst Thank you, this post helped me.

@espoirMur

This comment has been minimized.

Copy link

@espoirMur espoirMur commented Oct 27, 2020

In case, someone else likes the combination of both ideas by @ramast and @ikonst, I've adapted it to look up all fixtures, defined in their own file under the fixtures folder:

# conftest.py

from glob import glob


def refactor(string: str) -> str:
    return string.replace("/", ".").replace("\\", ".").replace(".py", "")


pytest_plugins = [
    refactor(fixture) for fixture in glob("tests/fixtures/*.py") if "__" not in fixture
]

So, this should work, if you project setup looks like this:

./
├── src
├── tests
│   ├── conftest.py
│   ├── fixtures
│   │   ├── my_fixture.py

edit: I noticed, that I was executing pytest from tests instead of root. Fixed the discovery path and shortened string manipulation.

Thanks a lot man.. this work like a charm for me..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.