67

Often I'll write a test class that uses a pytest fixture in every method. Here's an example. I'd like to be able to avoid having to write the fixture name in the signature of every method. It's not DRY. How can this be done?

I would like to be able to access the fixture by giving the fixture as an attribute of the test class. In this example, I would like to see the google fixture as an attribute of TestGoogle. Is this possible?

from bs4 import BeautifulSoup
import pytest
import requests

@pytest.fixture()
def google():
    return requests.get("https://www.google.com")


class TestGoogle:
    def test_alive(self, google):
        assert google.status_code == 200

    def test_html_title(self, google):
        soup = BeautifulSoup(google.content, "html.parser")
        assert soup.title.text.upper() == "GOOGLE"
Donal
  • 8,430
  • 3
  • 16
  • 21

3 Answers3

93

Sure, just use an autouse fixture. Here is the relevant spot in pytest docs. In your example, the change would be introducing an extra fixture (I named it _request_google_page):

from bs4 import BeautifulSoup
import pytest
import requests

@pytest.fixture()
def google():
    return requests.get("https://www.google.com")


class TestGoogle:

    @pytest.fixture(autouse=True)
    def _request_google_page(self, google):
        self._response = google

    def test_alive(self):
        assert self._response.status_code == 200

    def test_html_title(self):
        soup = BeautifulSoup(self._response.content, "html.parser")
        assert soup.title.text.upper() == "GOOGLE"

You could even drop the google fixture completely and move the code to _request_google_page:

@pytest.fixture(autouse=True)
def _request_google_page(self):
    self._response = requests.get("https://www.google.com")

Note that _request_google_page will be called once per test by default, so each test will get a new response. If you want the response to be initialized once and reused throughout all tests in the TestGoogle class, adjust the fixture scopes (scope='class' for _request_google_page and scope='module' or scope='session' for google). Example:

from bs4 import BeautifulSoup
import pytest
import requests


@pytest.fixture(scope='module')
def google():
    return requests.get("https://www.google.com")


@pytest.fixture(autouse=True, scope='class')
def _request_google_page(request, google):
    request.cls._response = google


class TestGoogle:

    def test_alive(self):
        assert self._response.status_code == 200

    def test_html_title(self):
        soup = BeautifulSoup(self._response.content, "html.parser")
        assert soup.title.text.upper() == "GOOGLE"
hoefling
  • 59,418
  • 12
  • 147
  • 194
  • Thanks @hoefling That's what I wanted. I never understood the autouse fixture functionality. I understand now. Cheers. – Donal May 03 '18 at 14:22
  • Glad I could help you! – hoefling May 04 '18 at 13:10
  • 1
    @hoefling for some reason this never works for me. The test methods seem to be getting a new instance and `self` does not have the attributes in the setup method. Just for context, I'm using 2 `pytext.mark...` on the class. – Ken4scholars Jan 15 '19 at 03:25
  • @Ken4scholars best is to ask a separate question, including a [mcve] of the failing test setup you have. Can you do that? – hoefling Jan 15 '19 at 08:06
  • @hoefling, sure. Please check this question. Though it is another issue there, but it has the description of this issue. https://stackoverflow.com/questions/54192519/django-channels-postgres-interfaceerror-connection-already-closed – Ken4scholars Jan 15 '19 at 09:27
  • @hoefling, When I set the scope _request_google_page to 'class' and the scope of google to "module", I see the following error inside the function "def test_alive(self):" AttributeError: 'TestGoogle' object has no attribute '_response' Any idea? – Prashant Pathak Jul 31 '20 at 16:42
  • @PrashantPathak I have added a snippet with adjusted scopes at the end of the answer, check it out. It will work when you copy it as-is. – hoefling Jul 31 '20 at 17:55
  • @hoefling, Thanks, So it means the function should be outside the class scope. – Prashant Pathak Aug 01 '20 at 14:59
  • @hoefling Can you also help me to understand one concept? which I am getting hard time to clear. So in the above code def _request_google_page() does not return anything still we are able to use response object in def test_alive(). So how the "request.cls._response" is getting accessed inside the class test methods, how is the call follow? – Prashant Pathak Aug 01 '20 at 15:03
  • 2
    @PrashantPathak this is part of test collection and preparation. `request.cls` will be `TestGoogle` when the `_request_google_page` fixture is evaluated on test collection, so after `TestGoogle` is collected, `TestGoogle._response` will be initialized; after that, `TestGoogle().test_alive()` is executed and `self._response` is available. It's a class variable, actually. – hoefling Aug 02 '20 at 10:36
  • @hoefling, Ohh I see so this "self" here is not an instance variable it's basically pointing to the class variable. Got it now. Thanks for your clarification – Prashant Pathak Aug 05 '20 at 15:26
  • I am facing another issue with it ScopeMismatch: You tried to access the 'function' scoped fixture with a 'class' scoped request object, involved factories – Anurag jain Apr 12 '21 at 18:37
  • @Anuragjain this means you have broadened the scope in a chain of fixtures invocation, which isn't allowed. This is not an issue with the code in the answer; if you need help, ask a new question with a [mcve]. – hoefling Apr 12 '21 at 19:19
  • The last example is horrendous. Please don't do this. Tests (well, all the code) should be easy to understand. Remember: "Explicit is better than implicit" – Marcin Krupowicz Dec 01 '22 at 15:10
35

I had to solve a similar problem and the accepted solution didn't work for me with a class-scoped fixture.

I wanted to call a fixture once per test class and re-use the value in test methods using self. This is actually what the OP was intending to do as well.

You can use the request fixture to access the class that's using it (request.cls) and assign the fixture value in a class attribute. Then you can access this attribute from self. Here's the full snippet:

from bs4 import BeautifulSoup
import pytest
import requests

@pytest.fixture(scope="class")
def google(request):
    request.cls.google = requests.get("https://www.google.com")


@pytest.mark.usefixtures("google")
class TestGoogle:
    def test_alive(self):
        assert self.google.status_code == 200

    def test_html_title(self):
        soup = BeautifulSoup(self.google.content, "html.parser")
        assert soup.title.text.upper() == "GOOGLE"

Hope that helps anyone else coming to this question.

makeiteasy
  • 766
  • 7
  • 11
  • However there is not way to set the value of `self.variable` through this? – MohitC Feb 04 '21 at 20:57
  • @MohitC I don't think there is and I can't think of a case when it would be needed and `cls` wouldn't work – makeiteasy Feb 05 '21 at 10:03
  • i actually want exactly this behaviour. set the value of the self.variable in a test and use that result in following tests – Jorrit Smit Apr 05 '21 at 11:14
  • 2
    @JorritSmit sounds strange to set a value in one test to be used in other tests. A test should test not generate values for other tests. – Jan Sakalos May 14 '21 at 17:38
1

Share same instance of object to all tests (or modular tests) using combination of conftest.py , fixtures and plain class

I'd like to share my solution, which comes from the Pytest documentation (7.2 currently)

Modularity: using fixtures from a fixture function

Now, if we combine it with a conftest.py this is what we could do: First having this structure (just to show sub-module and that, object is instanciated once for all tests)


├── conftest.py
├── sub_folder
│   ├── __init__.py
│   └── test_sub_1.py
├── test_1.py
├── test_2.py

This is the content conftest.py

"""tests/conftest.py"""
import pytest


class MockServer():
    def get(self, url):
        return "hello-world"

class App:
    def __init__(self, http_connection):
        print("APP CREATED")
        self.http_connection = http_connection

@pytest.fixture(scope="session")
def http_connection():
    print("HTTP_CONNECTION FIXTURE")
    return MockServer()

@pytest.fixture(scope="session")
def app(http_connection):
    print("CREATE APP")
    return App(http_connection)

tests/test_1.py

class TestClass1:

    def test_1(self, app):
        http_connection = app.http_connection

        assert http_connection.get("my-url") ==  "hello-world"

    def test_2(self, app):
        http_connection = app.http_connection

        assert http_connection.get("my-url") ==  "hello-world"

tests/test_2.py

class TestClass2:

    def test_1(self, app):
        http_connection = app.http_connection

        assert http_connection.get("my-url") ==  "hello-world"

    def test_2(self, app):
        http_connection = app.http_connection

        assert http_connection.get("my-url") ==  "hello-world"

tests/sub_folder/test_sub_1.py

"""tests/sub_folder/test_sub_1.py"""

class TestSubClass1:

    def test_sub_1(self, app):
        http_connection = app.http_connection

        assert http_connection.get("my-url") ==  "hello-world"

    def test_sub_2(self, app):
        http_connection = app.http_connection

        assert http_connection.get("my-url") ==  "hello-world"

Now, lets run

pytest -rP

The output should be something like

tests/test_1.py::TestClass1::test_1 PASSED                                                                                               [ 16%]
tests/test_1.py::TestClass1::test_2 PASSED                                                                                               [ 33%]
tests/test_2.py::TestClass2::test_1 PASSED                                                                                               [ 50%]
tests/test_2.py::TestClass2::test_2 PASSED                                                                                               [ 66%]
tests/sub_folder/test_sub_1.py::TestSubClass1::test_sub_1 PASSED                                                                         [ 83%]
tests/sub_folder/test_sub_1.py::TestSubClass1::test_sub_2 PASSED                                                                         [100%]

==================================================================== PASSES ====================================================================
______________________________________________________________ TestClass1.test_1 _______________________________________________________________
------------------------------------------------------------ Captured stdout setup -------------------------------------------------------------
HTTP_CONNECTION FIXTURE
CREATE APP
APP CREATED
============================================================== 6 passed in 0.23s ===============================================================

As you can see, by the output, (HTTP_CONNECTION FIXTURE, CREATE APP, APP CREATED) is run only once. That's good when we need to share reasource across all tests. Having said that, now let's combine it with hoefling answer, I only add tests/test_1.py

tests/test_1.py

"""tests/test_1.py"""
import pytest

class TestClass1:
    @pytest.fixture(autouse=True)
    def _app(self, app):
        self.app = app

    def test_1(self):
        assert self.app.http_connection.get("my-url") ==  "hello-world"

    def test_2(self):
        assert self.app.http_connection.get("my-url") ==  "hello-world"

That's already better, but we can take it a step further, let's have a Base test class and having our test inherit from it conftest.py

"""tests/conftest.py"""
import pytest


class MockServer():
    def get(self, url):
        return "hello-world"

class App:
    def __init__(self, http_connection):
        print("APP CREATED")
        self.http_connection = http_connection

@pytest.fixture(scope="session")
def http_connection():
    print("HTTP_CONNECTION FIXTURE")
    return MockServer()

@pytest.fixture(scope="session")
def app(http_connection):
    print("CREATE APP")
    return App(http_connection)


class Base:
    @pytest.fixture(autouse=True)
    def _app(self, app):
        self.app = app

Now """tests/test_1.py""" can look like

"""tests/test_1.py"""
from conftest import Base

class TestClass1(Base):

    def test_1(self):
        assert self.app.http_connection.get("my-url") ==  "hello-world"

    def test_2(self):
        assert self.app.http_connection.get("my-url") ==  "hello-world"

Federico Baù
  • 6,013
  • 5
  • 30
  • 38