2

I'm relatively new to pytest-style unit testing, and I'm trying to learn more about pytest fixtures. I'm not passing a scope argument to the fixture, so I know that the scope is "function". Are there any functional differences in these 3 styles of simple fixtures? Why would one approach be favored over the others?

@pytest.fixture()
@patch('a.b.c.structlog.get_logger')
def fixture_classQ(mock_log):
    handler = MagicMock(spec=WidgetHandler)
    return ClassQ(handler)
@pytest.fixture()
def fixture_classQ():
    with patch('a.b.c.structlog.get_logger'):
        handler = MagicMock(spec=WidgetHandler)
        return ClassQ(handler)
@pytest.yield_fixture()
def fixture_classQ():
    with patch('a.b.c.structlog.get_logger'):
        handler = MagicMock(spec=WidgetHandler)
        yield ClassQ(handler)

Simple example usage of the fixture:

def test_classQ_str(fixture_classQ):
    assert str(fixture_classQ) == "This is ClassQ"

Thanks.

Stone
  • 23
  • 3

1 Answers1

2

fixture 1

Starting with the first one, this creates a plain-data fixture. The mock is (imo misleadingly) only alive for the duration of the fixture function because it uses return.

In order ~roughly what happens for that:

  • pytest notices your fixture is used for the test function
  • it calls the fixture function
    • the mock decorator starts the patch
    • the mock decorator calls your actual function (which returns a value)
    • the mock decorator undoes the patch
  • pytest notices it wasn't a generator and so that's the value of your fixture

fixture 2

the second is identical in behaviour to the first, except it uses the context manager form of mock instead of the decorator. personally I don't like the decorator form but that's just me :D

fixture 3

(first before I continue, pytest.yield_fixture is a deprecated alias for pytest.fixture -- you can just use @pytest.fixture)

The third does something different! The patch is alive for the duration of the test because it has "yielded" during the fixture. This is a kind of way to create a setup + teardown fixture all in one. Here's roughly the execution here

  • pytest notices your fixture is used for the test function
  • pytest calls the fixture function
    • since it is a generator, it returns immediately without executing code
  • pytest notices it is a generator, calls next(...) on it
    • this causes the code to execute until the yield and then "pausing". you can think of it kind of as a co-routine
    • the __enter__ of the mock is called making the patch active
    • the value that is yielded is used as the value of the fixture
  • pytest then executes your test function
  • pytest then calls next(...) again on the generator to exhaust the fixture
    • this __exit__s the with statement, undoing the patch

which to choose?

the best answer is it depends. Since 1 and 2 are functionally equivalent it's up to personal preference. Pick 3. if you need the patch to be active during the entire duration of your test. And don't use pytest.yield_fixture, just use pytest.fixture.

anthony sottile
  • 61,815
  • 15
  • 148
  • 207
  • Excellent response, thank you. Could I get you to edit your answer and use "fixture function" and "test function" everywhere you used "function"? This would help clarify and ensure that I'm understanding correctly. – Stone Jan 22 '19 at 15:47
  • This is the best explanation of how fixtures work! Now I understand why my tests break when I use return instead of yield. – mgilsn Jul 05 '21 at 12:36