pytest-plone is a pytest plugin providing fixtures and helpers to test Plone add-ons.
This package is built on top of zope.pytestlayer.
Despite the fact Plone, and Zope, have their codebases tested with unittest, over the years
pytest became the most popular choice for testing in Python.
pytest is more flexible and easier to use than unittest and has a rich ecosystem of plugins that you can use to extend its functionality.
In your top-level conftest.py import your testing layers, and also import fixtures_factory -- which will accept a iterator of tuples containing the testing layer and a prefix to be used to generate the needed pytest fixtures.
from Products.CMFPlone.testing import PRODUCTS_CMFPLONE_FUNCTIONAL_TESTING
from Products.CMFPlone.testing import PRODUCTS_CMFPLONE_INTEGRATION_TESTING
from pytest_plone import fixtures_factory
pytest_plugins = ["pytest_plone"]
globals().update(
fixtures_factory(
(
(PRODUCTS_CMFPLONE_FUNCTIONAL_TESTING, "functional"),
(PRODUCTS_CMFPLONE_INTEGRATION_TESTING, "integration"),
)
)
)
In the code above, the following pytest fixtures will be available to your tests:
| Fixture |
Scope |
| functional_session |
Session |
| functional_class |
Class |
| functional |
Function |
| integration_session |
Session |
| integration_class |
Class |
| integration |
Function |
|
|
| Description |
Set environment variable to force Zope to compile translation files |
| Required Fixture |
|
| Scope |
Session |
Add a new fixture to your conftest.py to force generate_mo to be called for all tests.
@pytest.fixture(scope="session", autouse=True)
def session_initialization(generate_mo):
"""Fixture used to force translation files to be compiled."""
yield
|
|
| Description |
Zope root |
| Required Fixture |
integration |
| Scope |
Function |
def test_app(app):
"""Test portal title."""
assert app.getPhysicalPath() == ("", )
|
|
| Description |
Portal object |
| Required Fixture |
integration |
| Scope |
Function |
def test_portal_title(portal):
"""Test portal title."""
assert portal.title == "Plone Site"
|
|
| Description |
HTTP Request |
| Required Fixture |
integration |
| Scope |
Function |
from plone import api
def test_myproduct_controlpanel_view(portal, http_request):
"""Test myproduct_controlpanel browser view is available."""
view = api.content.get_view(
"myproduct-controlpanel", portal, http_request
)
assert view is not None
|
|
| Description |
Zope root bound to the functional testing layer. |
| Required Fixture |
functional |
| Scope |
Function |
Use this when you need a functional-layer counterpart to app — typically for REST API or browser tests.
def test_functional_app(functional_app):
"""Test app title."""
assert functional_app.getPhysicalPath() == ("", )
|
|
| Description |
Portal object bound to the functional testing layer. Honors @pytest.mark.portal. |
| Required Fixture |
functional |
| Scope |
Function |
Parallel to portal, but bound to the functional layer. Accepts the same @pytest.mark.portal marker for profiles, content, and roles — see the Markers section.
def test_functional_portal_title(functional_portal):
"""Test portal title on the functional layer."""
assert functional_portal.title == "Plone site"
|
|
| Description |
HTTP Request bound to the functional testing layer. |
| Required Fixture |
functional |
| Scope |
Function |
from plone import api
def test_functional_view(functional_portal, functional_http_request):
"""Test a browser view on the functional layer."""
view = api.content.get_view(
"myproduct-controlpanel", functional_portal, functional_http_request
)
assert view is not None
|
|
| Description |
Installer browser view. Used to install/uninstall/check for an add-on. |
| Required Fixture |
integration |
| Scope |
Function |
import pytest
PACKAGE_NAME = "myproduct"
@pytest.fixture
def uninstall(installer):
"""Fixture to uninstall a package."""
installer.uninstall_product(PACKAGE_NAME)
def test_product_installed(installer):
"""Test if myproduct is installed."""
assert installer.is_product_installed(PACKAGE_NAME) is True
@pytest.mark.parametrize(
"package",
[
"collective.casestudy",
"pytest-plone",
]
)
def test_dependency_installed(installer, package):
"""Test if dependency is installed."""
assert installer.is_product_installed(package) is True
|
|
| Description |
Uninstall the add-on under test from the current portal. |
| Required Fixture |
installer, package_name (user-provided) |
| Scope |
Function |
This fixture removes the duplicate per-project boilerplate from the canonical uninstall smoke test. You must define a package_name fixture in your conftest.py (or test module) that returns the distribution name of your add-on.
import pytest
@pytest.fixture
def package_name() -> str:
"""Distribution name of the add-on under test."""
return "collective.person"
class TestSetupUninstall:
@pytest.fixture(autouse=True)
def _uninstalled(self, uninstalled):
"""Uninstall the add-on before every test in this class."""
def test_product_uninstalled(self, installer, package_name):
assert installer.is_product_installed(package_name) is False
|
|
| Description |
List of available browser layers. Used to test if a specific browser layer is registered. |
| Required Fixture |
integration |
| Scope |
Function |
def test_browserlayer(browser_layers):
"""Test that IMyProductLayer is registered."""
from myproduct.interfaces import IMyProductLayer
assert IMyProductLayer in browser_layers
|
|
| Description |
List of control panel actions ids. Used to test if a specific control panel is installed or not. |
| Required Fixture |
integration |
| Scope |
Function |
def test_configlet_install(controlpanel_actions):
"""Test if control panel is installed."""
assert "myproductcontrolpanel" in controlpanel_actions
|
|
| Description |
Function to get the Factory Type Info (FTI) for a content type. |
| Required Fixture |
integration |
| Scope |
Function |
def test_get_fti(get_fti):
"""Test if Document fti is installed."""
assert get_fti("Document") is not None
|
|
| Description |
Function to list behaviors for a content type. |
| Required Fixture |
integration |
| Scope |
Function |
import pytest
def test_block_in_document(get_behaviors):
"""Test if blocks behavior is installed for Document."""
assert "volto.blocks" in get_behaviors("Document")
@pytest.mark.parametrize(
"behavior",
[
"plone.dublincore",
"plone.namefromtitle",
"plone.shortname",
"plone.excludefromnavigation",
"plone.relateditems",
"plone.versioning",
"volto.blocks",
"volto.navtitle",
"volto.preview_image",
"volto.head_title",
],
)
def test_has_behavior(self, get_behaviors, behavior):
assert behavior in get_behaviors("Document")
|
|
| Description |
Function to get a named vocabulary. |
| Required Fixture |
integration |
| Scope |
Function |
from zope.schema.vocabulary import SimpleVocabulary
VOCAB = "plone.app.vocabularies.AvailableContentLanguages"
def test_get_vocabulary(get_vocabulary):
"""Test plone.app.vocabularies.AvailableContentLanguages."""
vocab = get_vocabulary(VOCAB)
assert vocab is not None
assert isinstance(vocab, SimpleVocabulary)
|
|
| Description |
Portal Setup tool. |
| Required Fixture |
integration |
| Scope |
Function |
def test_setup_tool(setup_tool):
"""Test setup_tool."""
assert setup_tool is not None
|
|
| Description |
Function to get the last version of a profile. |
| Required Fixture |
integration |
| Scope |
Function |
PACKAGE_NAME = "collective.case_study"
def test_last_version(profile_last_version):
"""Test setup_tool."""
profile = f"{PACKAGE_NAME}:default"
version = profile_last_version(profile)
assert version == "1000"
|
|
| Description |
Function to apply GenericSetup profiles to a Plone site. |
| Required Fixture |
integration |
| Scope |
Session |
def test_with_profile(portal, apply_profiles):
"""Test that a profile can be applied."""
apply_profiles(portal, ["my.addon:testing"])
|
|
| Description |
Function to create content items in a Plone site as the site owner. |
| Required Fixture |
integration |
| Scope |
Session |
def test_with_content(portal, create_content):
"""Test that content is created."""
create_content(portal, [
{"type": "Document", "id": "doc1", "title": "A Document"},
])
assert "doc1" in portal
|
|
| Description |
Function to grant local roles to the test user on a given context. |
| Required Fixture |
integration |
| Scope |
Session |
def test_manager_action(portal, grant_roles):
"""Test an action that requires Manager role."""
grant_roles(portal, ["Manager"])
# test user now has Manager role on portal
|
|
| Description |
Callable that builds a RelativeSession against the functional portal. |
| Required Fixture |
functional_portal |
| Scope |
Function |
Replaces the 5+ near-identical request-session fixtures that downstream codebases reimplement. Returns a RelativeSession (a thin requests.Session subclass that resolves relative URLs against the portal's base URL) with sensible defaults:
role="Manager" — authenticate as the portal owner.
role="Anonymous" (default) — no authentication.
basic_auth=(user, password) — any other identity; takes precedence over role.
api=True (default) — suffix the base URL with ++api++ so relative calls hit the REST API.
Sessions are closed automatically at the end of the test.
def test_list_content(request_factory):
"""Test that the Manager role can list content."""
session = request_factory(role="Manager")
response = session.get("/")
assert response.status_code == 200
|
|
| Description |
RelativeSession pre-authenticated as the portal owner (Manager). |
| Required Fixture |
request_factory |
| Scope |
Function |
def test_controlpanels(manager_request):
"""Test listing of control panels."""
response = manager_request.get("/@controlpanels")
assert response.status_code == 200
|
|
| Description |
RelativeSession with no authentication (Anonymous). |
| Required Fixture |
request_factory |
| Scope |
Function |
def test_public_endpoint(anon_request):
"""Test a public REST API endpoint."""
response = anon_request.get("/")
assert response.status_code == 200
Configure the portal fixture with GenericSetup profiles, pre-created content, and/or user roles — without overriding the fixture.
| Parameter |
Type |
Description |
profiles |
list[str] |
GenericSetup profile IDs to apply (e.g. ["my.addon:testing"]) |
content |
list[dict] |
Dicts passed as keyword arguments to plone.api.content.create |
roles |
list[str] |
Roles granted to the test user via plone.api.user.grant_roles |
Setup is applied in order: profiles → content → roles.
import pytest
@pytest.mark.portal(
profiles=["my.addon:testing"],
content=[{"type": "Document", "id": "doc1", "title": "Doc 1"}],
roles=["Manager"],
)
def test_something(portal):
"""Test with custom portal setup."""
assert "doc1" in portal
Tests without the marker see no behavior change — fully backwards-compatible.
You need a working python environment (system, virtualenv, pyenv, etc) version 3.8 or superior.
Then install the dependencies and a development instance using:
To run tests for this package:
By default we use the latest Plone version in the 6.x series.
The project is licensed under the GPLv2.