Pytest and class scope

I play a little bit with the new pytest layer in an addon created with cookieplone

My goal: a functional test setup in a class with some sample content:

With the following setup all is fine. but the fixture is firing up in every method of my TestMyView class. This is not what i want. as far as I understand it, the reason is the scope of the fixture. in this case the "function scope".

# conftest.py

# global fixture to provide base structure for content
# can use in tests
@pytest.fixture
def contents_payload() -> list:
    """Payload to create two content items."""
    return [
        {
            "type": "Folder",
            "id": "test-folder",
            "title": "Test Folder",
            "description": "A Test Folder",
        },
        {
            "type": "Document",
            "id": "test-doc",
            "title": "Test Document",
            "description": "A Test Document",
        },
    ]

# fixture portal for functional test with test content
@pytest.fixture()
def portal(functional, content_payload):
    # the portal object for functional tests
    portal = functional["portal"]
    with api.env.adopt_roles(["Manager",]):
      for data in contents_payload:
        api.content.create(container=portal, **data)

    transaction.commit()
    return portal
# my Test Class
class TestMyView:
  def test_my_view1(self, portal):
    # do something in a functional test
    pass

  def test_my_view2(self, portal):
    # do something other stuff in a functional test
     pass

Now i changed the code to:

# fixture portal for functional test with test content
@pytest.fixture(scope="class")
def portal(functional_class, content_payload):
    # the portal object for functional tests
    portal = functional_class["portal"]
    with api.env.adopt_roles(["Manager",]):
      for data in contents_payload:
        api.content.create(container=portal, **data)

    transaction.commit()
    return portal

But this ends with an error:

self = <Layer 'collective.addon.testing.Collective.AddonLayer:FunctionalTesting'>, key = 'portal'

    def __getitem__(self, key):
        item = self.get(key, _marker)
        if item is _marker:
>           raise KeyError(key)
E           KeyError: 'portal'

.tox/test/lib/python3.12/site-packages/plone/testing/layer.py:28: KeyError

Where is my mistake? I thought, i can switch easily the scope, and no more changes are needed.

I'm new in the pytest world, please bear with me.

I looked into this, and from what I can tell it happens because of the details of how zope.pytestlayer creates pytest fixtures from Zope test layers.

The class-scoped fixtures do the layer setup (which creates the Plone site), but not the setup that is supposed to happen before each test (which activates the site with setSite and adds it to the layer as the "portal" resource). So, if you create a new class-scoped fixture that is based on the functional_class fixture, like you did, the "portal" resource isn't there.

One way to get the shared setup you want is to set up your content in the plone.app.testing layer's setUpPloneSite method, instead of in a pytest fixture.

I was able to make it work using pytest fixtures as follows. This is not very obvious; it needs to be documented better, and maybe we can add a simpler way to do this in pytest_plone.

# conftest.py
from plone.app.testing.helpers import ploneSite
from zope.pytestlayer.fixture import function_fixture

@pytest.fixture(scope="class")
def setup_content(functional_class, content_payload):
    """Add content to base Plone site"""
    # make sure we have a fully active portal
    with ploneSite() as portal:
        with api.env.adopt_roles(["Manager",]):
            for data in content_payload:
                api.content.create(container=portal, **data)

        transaction.commit()

@pytest.fixture
def functional(request, functional_class, setup_content):
    """Replace the function-scoped `functional` fixture 
    with one that uses our updated content.
    """
    return function_fixture(request, functional_class)


@pytest.fixture
def portal(functional):
    """Get the portal resource from our modified layer"""
    return functional["portal"]

Thanks for investigation and explanation, i will be check it tomorrow