Write your first test for a Plone add-on#

In this tutorial we will take an add-on that has no pytest tests, and give it one. By the end you will have a working conftest.py, a passing test, and a test that puts content into Plone before it runs.

What you need first#

You need an add-on with testing layers.

A testing layer is the object that builds a Plone site for your tests—it loads your ZCML and installs your profile. You declare yours in a testing.py module, and an add-on generated from a Plone template already has one. It defines two layers, conventionally named something like MY_ADDON_INTEGRATION_TESTING and MY_ADDON_FUNCTIONAL_TESTING.

Open your testing.py now and find those two names. We will use them in a moment.

If your add-on has no testing.py, write one before continuing; plone.app.testing explains how.

Install the plugin#

Add pytest-plone to your test dependencies and install them.

pip install pytest-plone

Create the conftest#

pytest finds fixtures in a file called conftest.py. Create one at the top of your package, next to your tests directory.

We import the testing layers the add-on already has, and hand them to fixtures_factory. It gives us back a dictionary of pytest fixtures, which we drop into the module’s namespace.

from my.addon.testing import MY_ADDON_FUNCTIONAL_TESTING
from my.addon.testing import MY_ADDON_INTEGRATION_TESTING
from pytest_plone import fixtures_factory


pytest_plugins = ["pytest_plone"]


globals().update(
    fixtures_factory(
        (
            (MY_ADDON_FUNCTIONAL_TESTING, "functional"),
            (MY_ADDON_INTEGRATION_TESTING, "integration"),
        )
    )
)

Replace my.addon with your own package name.

Notice that we did not write a single fixture ourselves. The two prefixes, integration and functional, are the names the generated fixtures will carry.

Write the test#

Create tests/test_first.py:

def test_portal_title(portal):
    assert portal.title == "Plone site"

Look closely at what is happening here. There is no class, no setUp, and no self. The test asks for a portal by naming it as an argument, and pytest builds one and hands it over.

Run it#

pytest tests/test_first.py

You should see one passing test:

tests/test_first.py .                                              [100%]

1 passed

The first run takes a few seconds, because Plone has to be set up. Run it again and notice that it does not get slower—the site is built once and kept for the whole session.

Ask for content#

Real tests need something to test against. pytest-plone can create content for you before the test runs, with a marker.

Add this to tests/test_first.py:

import pytest


@pytest.mark.portal(
    content=[{"type": "Document", "id": "doc1", "title": "My first document"}],
)
def test_document_exists(portal):
    assert "doc1" in portal
    assert portal["doc1"].title == "My first document"

Run the tests again:

pytest tests/test_first.py
tests/test_first.py ..                                             [100%]

2 passed

Both tests pass. Notice what did not happen: the document you created in the second test did not leak into the first one. Each test runs inside a transaction that is rolled back afterwards, so every test starts from a clean site.

What you have learned#

You have

  • turned your existing plone.testing layers into pytest fixtures with fixtures_factory,

  • written a test as a plain function that asks for a portal,

  • created content with @pytest.mark.portal without writing any setup code,

  • and seen that tests stay isolated from one another.

That is the whole model. Everything else pytest-plone offers is more fixtures of the same shape.

Where to go next#