pytest icon indicating copy to clipboard operation
pytest copied to clipboard

Pytest 8.4 fails to discover a test fixture withtin a test class when combined with the usage of freezegun

Open ngchihuan opened this issue 6 months ago • 15 comments

  • [x] a detailed description of the bug or problem you are having
  • [ ] output of pip list from the virtual environment you are using
  • [x] pytest and operating system versions
  • [x] minimal example if possible

Environment: pytest 8.4.1 freezegun 1.5.2

Description: Pytest 8.4 fails to discover a test fixture withtin a test class when combined with the usage of freezegun. The below minimal example runs with pytest 8.3.x

Minimal example:

import pytest
from freezegun import freeze_time

@freeze_time()
class TestA:
    @pytest.fixture
    def ff(self):
        return 1

    def test_a(self, ff):
        assert ff == 1

ngchihuan avatar Jun 03 '25 07:06 ngchihuan

it seems freezegun never directly supported fixtures and they just worked by accident

the latest release changed the type of fixture definitions from functions to a marker instance

freezeguns utilities to reshape the definition break pytest fixture discovery

we need to add a freezegun related regression test to pytest and add temporary backward compat in discovery

however this eventually needs a upstream fix in freezegun

RonnyPfannschmidt avatar Jun 03 '25 08:06 RonnyPfannschmidt

Same with responses. Looks like latest release will break a lot of tests

import responses
import pytest


@responses.activate
@pytest.fixture
def mock_response():
    responses.add(
        responses.GET,
        'https://api.example.com/data',
        json={'key': 'value'},
        status=200
    )
    yield responses


def test_api_call(mock_response):
    assert True
$ pytest test_resp.py                                                                      1 ↵
===================================== test session starts =====================================
platform linux -- Python 3.11.2, pytest-8.4.0, pluggy-1.5.0
rootdir: /home/gawel/tmp
plugins: Faker-37.1.0, libtmux-0.37.0, django-webtest-1.9.13
collected 1 item                                                                              

test_resp.py E                                                                          [100%]

=========================================== ERRORS ============================================
_______________________________ ERROR at setup of test_api_call _______________________________
file /home/gawel/tmp/test_resp.py, line 17
  def test_api_call(mock_response):
E       fixture 'mock_response' not found
>       available fixtures: _session_faker, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, capteesys, clear_env, config_file, django_app, django_app_factory, django_app_mixin, doctest_namespace, faker, home_path, home_user_name, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, server, session, session_params, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, user_path, zshrc
>       use 'pytest --fixtures [testpath]' for help on them.

gawel avatar Jun 03 '25 08:06 gawel

Hi @RonnyPfannschmidt!

the latest release changed the type of fixture definitions from functions to a marker instance

Can you expand a bit more on this? As a library developer, if I want to write a class-decorator that acts on all methods, how would I determine whether a method is a fixture?

I've opened a PR in freezegun where I look for the existence of the _pytestfixturefunction attribute, but I'm not sure whether that's the right way 👀 https://github.com/spulec/freezegun/pull/576/files

bblommers avatar Jun 07 '25 12:06 bblommers

We now have instances of fixture definition

They are currently still callable to raise a better error but a future release will remove the method altogether

RonnyPfannschmidt avatar Jun 07 '25 12:06 RonnyPfannschmidt

I took a quick look at the pr

Its not clear to me whether freezegun was ever correct for yield fixtures

RonnyPfannschmidt avatar Jun 07 '25 12:06 RonnyPfannschmidt

This bug just blew up the tests for our AWS python lambda. Looks like moto makes pytest 8.4.0 unhappy as well.

import pytest
import boto3
from moto import mock_aws

@mock_aws
@pytest.fixture
def ses_v1(base_env, mock_ses):
    return boto3.client('ses', region_name="us-east-1")


def test_api_call(ses_v1):
    assert True

Blows up with the same fixture 'ses_v1' not found error.

labrown avatar Jun 10 '25 15:06 labrown

Seems like we need to throw a error if a fixture gets wrapped

Decorating a defined fixture is always completely wrong and was completely incorrect since years

RonnyPfannschmidt avatar Jun 10 '25 16:06 RonnyPfannschmidt

Sounds like I need to use the context_manager version of mock_aws instead of the decorator, as below. The test_context_manager test passes.

import pytest
import boto3
from moto import mock_aws

@mock_aws
@pytest.fixture
def decorated_fixture():
    return boto3.client('ses', region_name="us-east-1")


@pytest.fixture
def context_manager_fixture():
    with mock_aws():
        return boto3.client('ses', region_name="us-east-1")

def test_decorated(decorated_fixture):
    assert True

def test_context_manager(context_manager_fixture):
    assert True

labrown avatar Jun 10 '25 18:06 labrown

Decorating a defined fixture is always completely wrong and was completely incorrect since years

We need to be able to provide library authors to at least be able to write correct code that handles pytest fixtures. This allows them to decide what to do with that information: error out, workaround it, etc.

We should consider making either FixtureFunctionMarker and FixtureFunctionDefinition public API, or provide some other public API to at least allow clients to inspect and verify that an object is a pytest fixture.

nicoddemus avatar Jun 12 '25 10:06 nicoddemus

Id like to introduce a base class for this purpose that will eventually expand in function

RonnyPfannschmidt avatar Jun 12 '25 10:06 RonnyPfannschmidt

Id like to introduce a base class for this purpose that will eventually expand in function

Could you exemplify?

provide some other public API to at least allow clients to inspect and verify that an object is a pytest fixture.

I'm leaning towards something like the above, using a clear-cut public API and avoid exposing any internals.

nicoddemus avatar Jun 12 '25 13:06 nicoddemus

Im thinking of introducing fixture definitions that are independent of the definition location support type annotation and possible parallel activation with different parameters

Its still too fuzzy to be concrete

RonnyPfannschmidt avatar Jun 12 '25 14:06 RonnyPfannschmidt

Its still too fuzzy to be concrete

Indeed I don't see a straightforward solution.

To outline the problem:

Prior to 8.4, @fixture returned a function, as is usual for decorators. Applying another decorator on top of it worked as expected.

Since 8.4, @fixture now returns an internal object, FixtureFunctionDefinition.

pytest now collects fixtures by looking for FixtureFunctionDefinition instances in the module namespace, which causes decorated fixtures to not be collected anymore, because the outer decorator returns a function. Even if they were collected, it would also break the decoration eventually, because calling FixtureFunctionDefinition directly raises a warning (and will error out in the future).

nicoddemus avatar Jun 13 '25 14:06 nicoddemus

Worth noting that, depending on the decorator, changing the order of the decorators might workaround the issue.

For example:

@some_decorator
@pytest.fixture
def foo(): ...

To:

@pytest.fixture
@some_decorator
def foo(): ...

nicoddemus avatar Jun 13 '25 15:06 nicoddemus

I'll check that with moto. Maybe

@pytest.fixture
@mock_aws
def mocked_ses(): ...

will work.

labrown avatar Jun 13 '25 15:06 labrown

As a cheap workaround (Python 3.13 syntax):

From:

import pytest
from freezegun import freeze_time


class Foo:
    ...


@freeze_time("2025-04-01 12:34:56")
class TestFoo:
    @pytest.fixture(name="foo")
    def fixture_foo(self) -> Foo:
        return Foo()

    def test_something(self, foo: Foo) -> None:
        assert foo

To:

import pytest
from collections.abc import Generator
from freezegun import freeze_time
from freezegun.api import StepTickTimeFactory, TickingDateTimeFactory, FrozenDateTimeFactory

type FreezeTimeFactory = StepTickTimeFactory | TickingDateTimeFactory | FrozenDateTimeFactory


@pytest.fixture(name="frozen_datetime")
def fixture_frozen_datetime() -> Generator[FreezeTimeFactory]:
    """Fixture to freeze the current datetime for testing purposes.

    Workaround for https://github.com/pytest-dev/pytest/issues/13479
    """
    with freeze_time("2025-04-01 12:34:56") as frozen_time:
        yield frozen_time



class Foo:
    ...


@pytest.mark.usefixtures("frozen_datetime")
class TestFoo:
    @pytest.fixture(name="foo")
    def fixture_foo(self) -> Foo:
        return Foo()

    def test_something(self, foo: Foo) -> None:
        assert foo

Mulugruntz avatar Jun 19 '25 13:06 Mulugruntz

Note that for freezegun specificially, there is pytest-dev/pytest-freezer you might want to use instead of hand-rolling a fixture.

The-Compiler avatar Jun 19 '25 15:06 The-Compiler

I'll check that with moto. Maybe

@pytest.fixture
@mock_aws
def mocked_ses(): ...

will work.

This fixed it for me, thanks!

mttwise avatar Jun 24 '25 18:06 mttwise

Freezegun should be working now https://github.com/spulec/freezegun/issues/575#issuecomment-3065959304

nhtgl avatar Jul 15 '25 14:07 nhtgl