6

As the documentation and this article state, it should be possible to use hypothesis strategies and pytest fixtures in the same test.

But executing this example code of the article:

from hypothesis import given, strategies as st
from pytest import fixture


@fixture
def stuff():
    return "kittens"


@given(a=st.none())
def test_stuff(a, stuff):
    assert a is None
    assert stuff == "kittens"

yields the following error:

FAILED                                  [100%]
test_with_fixture.py:9 (test_stuff)
item = <Function test_stuff>

    @pytest.hookimpl(hookwrapper=True)
    def pytest_runtest_call(item):
        if not (hasattr(item, "obj") and "hypothesis" in sys.modules):
            yield
            return
    
        from hypothesis import core
        from hypothesis.internal.detection import is_hypothesis_test
    
        core.running_under_pytest = True
    
        if not is_hypothesis_test(item.obj):
            # If @given was not applied, check whether other hypothesis
            # decorators were applied, and raise an error if they were.
            if getattr(item.obj, "is_hypothesis_strategy_function", False):
                from hypothesis.errors import InvalidArgument
    
                raise InvalidArgument(
                    f"{item.nodeid} is a function that returns a Hypothesis strategy, "
                    "but pytest has collected it as a test function.  This is useless "
                    "as the function body will never be executed.  To define a test "
                    "function, use @given instead of @composite."
                )
            message = "Using `@%s` on a test without `@given` is completely pointless."
            for name, attribute in [
                ("example", "hypothesis_explicit_examples"),
                ("seed", "_hypothesis_internal_use_seed"),
                ("settings", "_hypothesis_internal_settings_applied"),
                ("reproduce_example", "_hypothesis_internal_use_reproduce_failure"),
            ]:
                if hasattr(item.obj, attribute):
                    from hypothesis.errors import InvalidArgument
    
                    raise InvalidArgument(message % (name,))
            yield
        else:
            from hypothesis import HealthCheck, settings
            from hypothesis.internal.escalation import current_pytest_item
            from hypothesis.internal.healthcheck import fail_health_check
            from hypothesis.reporting import with_reporter
            from hypothesis.statistics import collector, describe_statistics
    
            # Retrieve the settings for this test from the test object, which
            # is normally a Hypothesis wrapped_test wrapper. If this doesn't
            # work, the test object is probably something weird
            # (e.g a stateful test wrapper), so we skip the function-scoped
            # fixture check.
            settings = getattr(item.obj, "_hypothesis_internal_use_settings", None)
    
            # Check for suspicious use of function-scoped fixtures, but only
            # if the corresponding health check is not suppressed.
            if (
                settings is not None
                and HealthCheck.function_scoped_fixture
                not in settings.suppress_health_check
            ):
                # Warn about function-scoped fixtures, excluding autouse fixtures because
                # the advice is probably not actionable and the status quo seems OK...
                # See https://github.com/HypothesisWorks/hypothesis/issues/377 for detail.
                argnames = None
                for fx_defs in item._request._fixturemanager.getfixtureinfo(
                    node=item, func=item.function, cls=None
                ).name2fixturedefs.values():
                    if argnames is None:
                        argnames = frozenset(signature(item.function).parameters)
                    for fx in fx_defs:
                        if fx.argname in argnames:
                            active_fx = item._request._get_active_fixturedef(fx.argname)
                            if active_fx.scope == "function":
>                               fail_health_check(
                                    settings,
                                    _FIXTURE_MSG.format(fx.argname, item.nodeid),
                                    HealthCheck.function_scoped_fixture,
                                )
E                               hypothesis.errors.FailedHealthCheck: Function-scoped fixture 'stuff' used by 'test_with_fixture.py::test_stuff'
E                               
E                               Function-scoped fixtures are not reset between examples generated by
E                               `@given(...)`, which is often surprising and can cause subtle test bugs.
E                               
E                               If you were expecting the fixture to run separately for each generated example,
E                               then unfortunately you will need to find a different way to achieve your goal
E                               (e.g. using a similar context manager instead of a fixture).
E                               
E                               If you are confident that your test will work correctly even though the
E                               fixture is not reset between generated examples, you can suppress this health
E                               check to assure Hypothesis that you understand what you are doing.
E                               
E                               See https://hypothesis.readthedocs.io/en/latest/healthchecks.html for more information about this. If you want to disable just this health check, add HealthCheck.function_scoped_fixture to the suppress_health_check settings for this test.

../.venv/lib/python3.10/site-packages/_hypothesis_pytestplugin.py:250: FailedHealthCheck

I am using pytest version 7.1.2 and hypothesis version 6.47.5

What am I doing wrong?

Is there another way to use pytest fixtures and hypothesis strategies together?

Dennis
  • 71
  • 5

1 Answers1

4

Everything is working as intended. There is a warning that comes across as an error on the bottom of your output that directs you to this link to inform you about using function scoped fixtures, which is the default for a fixture when no scope argument is provided. You can read more about why function scoped fixtures are not ideal for hypothesis as shown here.

If you want to disregard and get around this you can just apply the settings decorator and suppress this warning as shown below.

from hypothesis import given, strategies as st, settings, HealthCheck
from pytest import fixture


@fixture
def stuff():
    return "kittens"


@given(a=st.none())
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture])
def test_stuff(a, stuff):
    assert a is None
    assert stuff == "kittens"

============================================================ test session starts ============================================================
platform darwin -- Python 3.8.9, pytest-7.0.1, pluggy-1.0.0
rootdir: ***
plugins: asyncio-0.18.3, hypothesis-6.48.1, mock-3.7.0
asyncio: mode=strict
collected 1 item                                                                                                                            

tests/test_script.py .                                                                                                                [100%]

============================================================= 1 passed in 0.06s =============================================================
gold_cy
  • 13,648
  • 3
  • 23
  • 45
  • Thanks a lot. It would be great if this was also state as clearly in the article, but maybe the article was written before the healthchecks were implemented. I guess a workaround without suppressing is to widen the scope of the fixture and to use factory fixtures if one really wants to execute the fixture function at every call of the test function. – Dennis Jun 29 '22 at 04:56
  • the article you linked is from 2016 so not surprising it may not capture everything. as for your second point yes, widening the scope is the recommended approach. – gold_cy Jun 29 '22 at 10:53