From ab4f77939bdea9496d768dd2d13fb6ad348388c2 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Sat, 25 Jul 2026 00:57:24 +0200 Subject: [PATCH 1/4] Add a Testing API reference covering plone.app.testing and plone.testing Extends the developer-guide/testing chapter with an autodoc-generated API reference and a deeper layer-model explanation, covering both testing packages as one toolkit (a test author does not care which package a symbol lives in). - testing-api-reference.md: organized by task (layer model, pre-built layers and fixtures, browser and HTTP, helpers, sandboxing primitives, constants), pulling symbols from both packages at their canonical location, with re-exports noted. Generated by sphinx-autodoc2, which reads the sources statically -- so no Plone is installed in the docs build. Both packages are added as source-only submodules. - how-testing-layers-work.md: deepened with the fixture/lifecycle split. - install-add-ons-in-tests.md, drive-the-test-browser.md: two how-tos that absorb the test-specific patterns from plone.app.testing's README. conf.py gains the autodoc2 extension beside the existing sphinx.ext.autodoc (used by plone.api); the two coexist. Legacy ZServer is noted, not documented. Vale is clean on the new pages. --- .gitmodules | 8 + docs/conf.py | 22 ++ .../testing/drive-the-test-browser.md | 92 ++++++++ .../testing/how-testing-layers-work.md | 106 +++++++++ docs/developer-guide/testing/index.md | 18 ++ .../testing/install-add-ons-in-tests.md | 90 +++++++ .../testing/testing-api-reference.md | 219 ++++++++++++++++++ requirements.txt | 1 + styles/config/vocabularies/Plone/accept.txt | 9 + submodules/plone.app.testing | 1 + submodules/plone.testing | 1 + 11 files changed, 567 insertions(+) create mode 100644 docs/developer-guide/testing/drive-the-test-browser.md create mode 100644 docs/developer-guide/testing/how-testing-layers-work.md create mode 100644 docs/developer-guide/testing/install-add-ons-in-tests.md create mode 100644 docs/developer-guide/testing/testing-api-reference.md create mode 160000 submodules/plone.app.testing create mode 160000 submodules/plone.testing diff --git a/.gitmodules b/.gitmodules index 0e7410d058..9f95e13a80 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,11 @@ path = submodules/plone.api url = https://github.com/plone/plone.api.git branch = main +[submodule "submodules/plone.app.testing"] + path = submodules/plone.app.testing + url = https://github.com/plone/plone.app.testing.git + branch = master +[submodule "submodules/plone.testing"] + path = submodules/plone.testing + url = https://github.com/plone/plone.testing.git + branch = master diff --git a/docs/conf.py b/docs/conf.py index ce9fd132d1..d56eab0291 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,6 +45,7 @@ extensions = [ "myst_parser", "notfound.extension", + "autodoc2", # developer-guide/testing plone.app.testing reference (static, no import) "sphinx.ext.autodoc", "sphinx.ext.autosummary", # plone.api "sphinx.ext.doctest", # plone.api @@ -301,6 +302,26 @@ # Don't show class signature with the class' name. autodoc_class_signature = "separated" +# -- Options for autodoc2 (plone.app.testing reference) ----------------------- +# autodoc2 analyses the source statically, so no package import and no Plone +# installation are needed. It reads the plone.app.testing submodule directly. +# The ``module`` key gives the namespace package its full dotted name. +autodoc2_packages = [ + { + "path": "../submodules/plone.app.testing/src/plone/app/testing", + "module": "plone.app.testing", + "auto_mode": False, + }, + { + "path": "../submodules/plone.testing/src/plone/testing", + "module": "plone.testing", + "auto_mode": False, + }, +] +autodoc2_render_plugin = "myst" +# The plone.app.testing docstrings are reStructuredText, not MyST. +autodoc2_docstring_parser_regexes = [(r".*", "rst")] + # -- Options for MyST markdown conversion to HTML ----------------------------- # For more information see: @@ -310,6 +331,7 @@ "attrs_inline", # Support parsing of inline attributes. "colon_fence", # You can also use ::: delimiters to denote code fences, instead of ```. "deflist", # Support definition lists. https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#definition-lists + "fieldlist", # Render reST field lists (:param:) from autodoc2 docstrings. "html_image", # For inline images. See https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#html-images "linkify", # Identify "bare" web URLs and add hyperlinks. "strikethrough", # See https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#syntax-strikethrough diff --git a/docs/developer-guide/testing/drive-the-test-browser.md b/docs/developer-guide/testing/drive-the-test-browser.md new file mode 100644 index 0000000000..406e62b056 --- /dev/null +++ b/docs/developer-guide/testing/drive-the-test-browser.md @@ -0,0 +1,92 @@ +--- +myst: + html_meta: + "description": "How to drive Plone with zope.testbrowser in a functional test: open URLs, follow links, submit forms." + "property=og:description": "How to drive Plone with zope.testbrowser in a functional test: open URLs, follow links, submit forms." + "property=og:title": "Drive the test browser" + "keywords": "Plone, testing, zope.testbrowser, functional test, browser" +--- + +(drive-the-test-browser)= + +# Drive the test browser + +This guide shows you how to write an end-to-end functional test that drives Plone through `zope.testbrowser`, which acts as a web browser connected to Zope in-process. + +```{important} +`zope.testbrowser` runs entirely in Python and does **not** run JavaScript. +Use it for server-rendered pages (Classic UI). To test a Volto frontend, use the Volto documentation's end-to-end testing tools instead. +``` + +You need a **functional** layer, either `PLONE_FUNCTIONAL_TESTING` or your own layer built with `FunctionalTesting`. +The test browser cannot see an integration layer's uncommitted transaction; see {ref}`how-testing-layers-work` for why. + +## Get a browser + +```python +from plone.testing.zope import Browser + +browser = Browser(app) +``` + +`app` is the Zope root (the `app` resource from the layer). + +## Make content visible to the browser + +The browser runs in a separate transaction, so it sees only **committed** data. +If a test creates content and then visits it, commit first: + +```python +import transaction +from plone.app.testing import setRoles, TEST_USER_ID + +setRoles(portal, TEST_USER_ID, ["Manager"]) +portal.invokeFactory("Folder", "f1", title="Folder 1") +setRoles(portal, TEST_USER_ID, ["Member"]) + +transaction.commit() # now the browser can see f1 +``` + +## Open a page and inspect it + +```python +browser.open(portal.absolute_url()) + +assert "Welcome" in browser.contents +assert browser.headers["content-type"] == "text/html; charset=utf-8" +``` + +## Follow links + +```python +browser.getLink("Edit").click() # by link text +browser.getLink(id="edit-link").click() # by HTML id + +assert browser.url == portal.absolute_url() + "/edit" +``` + +## Fill in and submit a form + +```python +browser.getControl("Age").value = "30" # by the control's label +browser.getControl(name="age:int").value = "30" # by form variable name + +browser.getControl("Save").click() # submit by button label +``` + +See the [zope.testbrowser documentation](https://github.com/zopefoundation/zope.testbrowser) for selecting and manipulating every control type. + +## Debugging + +When a submission does not do what you expect, print the response to see what the browser actually got: + +```python +print(browser.contents) +``` + +An unhandled exception on the server is re-raised in the test by default, so you get the real traceback rather than a rendered error page. + +```{seealso} +- {ref}`how-testing-layers-work`—why functional layers commit and integration layers do not. +- pytest users testing the REST API rather than HTML: {doc}`pytest` and pytest-plone's request fixtures. +``` diff --git a/docs/developer-guide/testing/how-testing-layers-work.md b/docs/developer-guide/testing/how-testing-layers-work.md new file mode 100644 index 0000000000..d11116c600 --- /dev/null +++ b/docs/developer-guide/testing/how-testing-layers-work.md @@ -0,0 +1,106 @@ +--- +myst: + html_meta: + "description": "How Plone testing layers work: the plone.testing layer model, resources, bases, sandboxing, and isolation." + "property=og:description": "How Plone testing layers work: the plone.testing layer model, resources, bases, sandboxing, and isolation." + "property=og:title": "How testing layers work" + "keywords": "Plone, testing, plone.testing, layers, DemoStorage, component registry, isolation" +--- + +(how-testing-layers-work)= + +# How testing layers work + +This page explains the machinery beneath the testing layers—the [plone.testing](https://github.com/plone/plone.testing) model that [plone.app.testing](https://github.com/plone/plone.app.testing) builds on. + +You do not need this to write ordinary tests. +Read it when you write a non-trivial base layer, or when you need to understand why a layer behaves the way it does. + +For *why* Plone tests with layers at all, and the trade-off between integration and functional testing, read {ref}`about-testing-in-plone` first. +For the classes and helpers named here, see {doc}`testing-api-reference`. + +## A layer is an object with a lifecycle + +A layer is an object with four lifecycle methods: + +- `setUp` and `tearDown` run **once**, around the whole group of tests that share the layer. + This is where the expensive work goes—start Zope, create the Plone site, load ZCML, install a profile. +- `testSetUp` and `testTearDown` run **around every test**. This is where the cheap per-test isolation goes. + +Splitting the expensive from the cheap is the entire point: the costly setup happens once and is shared, while each test still starts from a clean state. + +## Layers share state through resources + +A layer is also a mapping. +Set-up code stores things in it by key, and tests read them back: + +```python +def setUp(self): + self["app"] = ... # the Zope root +``` + +```python +def test_something(self): + app = self.layer["app"] +``` + +Resources are **stacked**. +When a layer sets a key that one of its bases already set, the layer's value shadows the base's for the duration of that layer, and the base's value reappears when the layer tears down. +This is how a functional layer can, for example, replace the database with a sandboxed copy without disturbing the layer it builds on. + +## Layers compose through bases + +A layer declares its bases—the layers it builds on—through `defaultBases` or the `bases` argument. +The test runner sets up each base once, in order, before the layer itself, and reuses an already-set-up base rather than building it again. + +The result is a tree of layers, each built once. +A typical add-on's stack looks like this: + +```text +Zope startup (plone.testing.zope.STARTUP) + └─ PloneFixture (a Plone site: PLONE_FIXTURE) + └─ your fixture (your PloneSandboxLayer subclass: loads ZCML, installs your profile) + ├─ IntegrationTesting (per-test transaction) + └─ FunctionalTesting (per-test DemoStorage) +``` + +`PLONE_FIXTURE` sits in the middle: it is the shared Plone site every add-on layer builds on. +You never use it in a test directly—you build your own fixture on it, then derive integration and functional layers from that. + +Notice that both `IntegrationTesting` and `FunctionalTesting` are built on the *same* fixture, by passing it as their `bases`. +This is the point of separating the fixture from the lifecycle: the expensive site is built once, and the two layers add only the cheap per-test behavior on top. +The same trick lets a package reuse a base layer with a different lifecycle, or add a second fixture beside it, without paying for the expensive setup twice. + +## How isolation works + +Isolation happens in the cheap per-test half, and the two layer kinds do it differently. + +An **integration** layer begins a transaction in `testSetUp` and **aborts** it in `testTearDown`. +Nothing a test writes is committed, so the next test sees the pristine site. +This is fast, and it is what most tests use. + +A **functional** layer instead stacks a temporary `DemoStorage` on the database in `testSetUp` and discards it in `testTearDown`. +The test may **commit** for real, and a separate process—a browser, an HTTP client—can see the result, because the data really is in the (sandboxed) database. +When the test ends, the whole stacked storage is thrown away. +This is more expensive, which is why you reach for it only when a request has to travel over the network. + +(zca-sandbox)= + +## The component-registry sandbox + +Plone relies heavily on the Zope Component Architecture—a global registry of components, populated by loading ZCML. +If a test layer loaded ZCML into the one global registry and never undid it, registrations would leak from one layer into the next, and tests would interfere with each other in ways that depend on run order. + +To prevent this, a sandboxing layer **pushes a new component registry** on set-up and **pops it** on tear-down, so every registration it makes lives only as long as the layer. +`PloneSandboxLayer` does this for you—that is what the "sandbox" in its name means. +The primitives are {ref}`pushGlobalRegistry and popGlobalRegistry `; you rarely call them directly. + +## Server fixtures for real HTTP + +An in-process functional test can drive Plone through a test browser without a socket. +When a test needs a **real** HTTP server—a live URL that an external client hits—add `WSGI_SERVER_FIXTURE` (from `plone.testing.zope`) to the functional layer's bases. +It starts a WSGI server for the duration of the layer and exposes its address, so requests genuinely travel over the network. + +```{seealso} +The full model, including the ZODB and component-architecture helpers, lives in [plone.testing](https://github.com/plone/plone.testing/blob/master/src/plone/testing/README.rst). +``` diff --git a/docs/developer-guide/testing/index.md b/docs/developer-guide/testing/index.md index d9a3dfef99..e382b8d356 100644 --- a/docs/developer-guide/testing/index.md +++ b/docs/developer-guide/testing/index.md @@ -49,10 +49,28 @@ Choose pytest for new work. Do not use both in one package. +## Guides + +- {doc}`write-a-testing-layer`: the `testing.py` layer both runners share. +- {doc}`zope-testrunner`: write and run tests with `unittest` and `zope.testrunner`. +- {doc}`pytest`: write and run tests with pytest. +- {doc}`install-add-ons-in-tests`: apply profiles and install add-ons in a test. +- {doc}`drive-the-test-browser`: end-to-end tests with `zope.testbrowser`. + +## Reference and background + +- {doc}`testing-api-reference`: the layers, fixtures, helpers, and sandboxing, from both packages. +- {doc}`how-testing-layers-work`: the layer model beneath it all. + ```{toctree} +:hidden: :maxdepth: 1 write-a-testing-layer zope-testrunner pytest +install-add-ons-in-tests +drive-the-test-browser +testing-api-reference +how-testing-layers-work ``` diff --git a/docs/developer-guide/testing/install-add-ons-in-tests.md b/docs/developer-guide/testing/install-add-ons-in-tests.md new file mode 100644 index 0000000000..129ee91ffe --- /dev/null +++ b/docs/developer-guide/testing/install-add-ons-in-tests.md @@ -0,0 +1,90 @@ +--- +myst: + html_meta: + "description": "How to install add-ons and GenericSetup profiles in a Plone test, and verify the result." + "property=og:description": "How to install add-ons and GenericSetup profiles in a Plone test, and verify the result." + "property=og:title": "Install add-ons and profiles in a test" + "keywords": "Plone, testing, applyProfile, quickInstallProduct, GenericSetup, add-on" +--- + +(install-add-ons-in-tests)= + +# Install add-ons and profiles in a test + +This guide shows you how to install a GenericSetup profile or an add-on inside a test, and how to check that the installation did what you expect. + +It uses the helpers from {doc}`testing-api-reference`. +The examples assume a layer whose fixture already loaded your add-on's ZCML—see {doc}`write-a-testing-layer`. + +If you use pytest, {doc}`pytest-plone ` also offers an `installer` fixture and an `@pytest.mark.portal(profiles=[...])` marker that do the same thing with less boilerplate. + +## Apply a profile + +The preferred way to install an add-on's configuration is to apply its GenericSetup profile: + +```python +from plone.app.testing import applyProfile + +applyProfile(portal, "my.addon:default") +``` + +You would usually do this once in your layer's `setUpPloneSite`, so every test in the layer runs against the installed add-on. +Do it in an individual test only when you are testing the installation itself. + +## Install through the add-ons control panel + +To install exactly as a site administrator would, through the add-ons control panel: + +```python +from plone.app.testing import quickInstallProduct + +quickInstallProduct(portal, "my.addon") +``` + +To force a reinstall—uninstall, then install again: + +```python +quickInstallProduct(portal, "my.addon", reinstall=True) +``` + +Both assume the add-on's ZCML has been loaded, which the layer set-up normally does. + +## Verify the installation + +When you write an add-on with an install profile, you usually want a test that the profile did its job. + +Check the add-on is installed: + +```python +from plone.base.utils import get_installer + +installer = get_installer(portal) +assert installer.is_product_installed("my.addon") +``` + +Check a content type was registered (via `types.xml`): + +```python +types_tool = portal.portal_types +assert types_tool.getTypeInfo("MyType") is not None +``` + +Check a catalog index was added (via `catalog.xml`): + +```python +catalog = portal.portal_catalog +assert "my_index" in catalog.indexes() +``` + +Check a workflow was installed and assigned (via `workflows.xml`): + +```python +workflow_tool = portal.portal_workflow +assert workflow_tool.getWorkflowById("my_workflow") is not None +assert dict(workflow_tool.listChainOverrides())["MyType"] == ("my_workflow",) +``` + +```{seealso} +- {doc}`testing-api-reference`—`applyProfile`, `quickInstallProduct`, and the rest. +- The uninstall side of this suite: pytest-plone's `uninstalled` fixture, in {doc}`pytest`. +``` diff --git a/docs/developer-guide/testing/testing-api-reference.md b/docs/developer-guide/testing/testing-api-reference.md new file mode 100644 index 0000000000..b6090d8f72 --- /dev/null +++ b/docs/developer-guide/testing/testing-api-reference.md @@ -0,0 +1,219 @@ +--- +myst: + html_meta: + "description": "Reference for the Plone testing API: layers, fixtures, helpers, and sandboxing, from plone.app.testing and plone.testing." + "property=og:description": "Reference for the Plone testing API: layers, fixtures, helpers, and sandboxing, from plone.app.testing and plone.testing." + "property=og:title": "Testing API reference" + "keywords": "Plone, testing, plone.app.testing, plone.testing, layers, fixtures, helpers" +--- + +(testing-api-reference)= + +# Testing API reference + +Technical reference for Plone's testing API, drawn from two packages: + +- [plone.app.testing](https://github.com/plone/plone.app.testing): the Plone-specific layers and helpers. +- [plone.testing](https://github.com/plone/plone.testing): the underlying layer model and the Zope-level tools. + +You import from whichever package a symbol lives in, but from a test author's point of view they are one toolkit, so this page is organized by task rather than by package. +Each symbol is documented at its canonical location; re-exports are noted. + +To use these to write a `testing.py`, see {doc}`write-a-testing-layer`. +For the model behind them, see {doc}`how-testing-layers-work`. +Signatures are generated from the source, so they always match the installed version. + +## The layer model + +The base class and the composition helper come from `plone.testing`; the Plone-specific layers build on them. + +```{autodoc2-object} plone.testing.layer.Layer +render_plugin = "myst" +``` + +```{autodoc2-object} plone.testing.layer.layered +render_plugin = "myst" +``` + +```{autodoc2-object} plone.app.testing.helpers.PloneSandboxLayer +render_plugin = "myst" +``` + +```{autodoc2-object} plone.app.testing.helpers.PloneWithPackageLayer +render_plugin = "myst" +``` + +```{autodoc2-object} plone.app.testing.layers.IntegrationTesting +render_plugin = "myst" +``` + +```{autodoc2-object} plone.app.testing.layers.FunctionalTesting +render_plugin = "myst" +``` + +```{autodoc2-object} plone.app.testing.layers.PloneFixture +render_plugin = "myst" +``` + +```{autodoc2-object} plone.app.testing.layers.PloneTestLifecycle +render_plugin = "myst" +``` + +## Pre-built layers and fixtures + +Ready-made layer instances. +The `PLONE_*` layers give you a plain Plone site; the `plone.testing` fixtures are lower-level building blocks you add to a layer's bases. + +| Layer / fixture | Package | Use | +| --- | --- | --- | +| `PLONE_FIXTURE` | plone.app.testing | A plain Plone site. The base you build your own fixture on, not used in tests directly. | +| `PLONE_INTEGRATION_TESTING` | plone.app.testing | A ready integration layer for a plain Plone site. | +| `PLONE_FUNCTIONAL_TESTING` | plone.app.testing | A ready functional layer for a plain Plone site. | +| `MOCK_MAILHOST_FIXTURE` | plone.app.testing | Replaces the mail host so tests can capture outgoing email. | +| `STARTUP` | plone.testing.zope | Starts Zope. The root of most layer stacks. | +| `WSGI_SERVER_FIXTURE` | plone.testing.zope | Runs a real WSGI server, for tests that make requests over a socket. Add it to a functional layer's bases. | +| `UNIT_TESTING` | plone.testing.zca | An isolated component registry for tests that need one without a full site. | +| `EMPTY_ZODB` | plone.testing.zodb | An empty database. | +| `LAYER_CLEANUP` | plone.testing.zca | Runs the `zope.testing` cleanup handlers on tear-down. | + +```{note} +`PLONE_ZSERVER` and `PLONE_FTP_SERVER` exist for the legacy ZServer. +Plone 6 runs on WSGI; use `WSGI_SERVER_FIXTURE` for a real HTTP server instead. +``` + +## Browser and HTTP + +For end-to-end tests that drive Plone as a browser would. +See {doc}`drive-the-test-browser` for the task-oriented guide. + +### `Browser` + +```python +from plone.testing.zope import Browser + +browser = Browser(app) +``` + +A [zope.testbrowser](https://github.com/zopefoundation/zope.testbrowser) browser wired to the Zope application under test. +It speaks to Zope in-process, over a special channel rather than a real socket, so it needs a functional layer but not `WSGI_SERVER_FIXTURE`. +`app` is the Zope root, the `app` resource from the layer. +See the [zope.testbrowser documentation](https://github.com/zopefoundation/zope.testbrowser) for the full browser API. + +### `zopeApp` + +```{autodoc2-object} plone.testing.zope.zopeApp +render_plugin = "myst" +``` + +## Helpers + +### Users and roles + +These are the `plone.app.testing` helpers; they wrap the lower-level `plone.testing.zope` versions and are the ones you normally import. + +```{autodoc2-object} plone.app.testing.helpers.login +render_plugin = "myst" +``` + +```{autodoc2-object} plone.app.testing.helpers.logout +render_plugin = "myst" +``` + +```{autodoc2-object} plone.app.testing.helpers.setRoles +render_plugin = "myst" +``` + +### Products and profiles + +`applyProfile` and `quickInstallProduct` install Plone add-ons; `installProduct` and `uninstallProduct` register a Zope product at the Zope level, which you usually only need in a layer's `setUpZope`. + +```{autodoc2-object} plone.app.testing.helpers.applyProfile +render_plugin = "myst" +``` + +```{autodoc2-object} plone.app.testing.helpers.quickInstallProduct +render_plugin = "myst" +``` + +```{autodoc2-object} plone.testing.zope.installProduct +render_plugin = "myst" +``` + +```{autodoc2-object} plone.testing.zope.uninstallProduct +render_plugin = "myst" +``` + +### Working with the site + +```{autodoc2-object} plone.app.testing.helpers.ploneSite +render_plugin = "myst" +``` + +## Sandboxing primitives + +Set-up code uses these to keep global state from leaking between layers. +`PloneSandboxLayer` calls them for you, so you rarely need them directly. +For why the sandboxing exists, see {ref}`the component-registry sandbox `. + +The component registry (populated by ZCML): + +```{autodoc2-object} plone.testing.zca.pushGlobalRegistry +render_plugin = "myst" +``` + +```{autodoc2-object} plone.testing.zca.popGlobalRegistry +render_plugin = "myst" +``` + +```{note} +`plone.app.testing` re-exports `pushGlobalRegistry` and `popGlobalRegistry`, so `from plone.app.testing import pushGlobalRegistry` also works. +``` + +The security checkers: + +```{autodoc2-object} plone.testing.security.pushCheckers +render_plugin = "myst" +``` + +```{autodoc2-object} plone.testing.security.popCheckers +render_plugin = "myst" +``` + +The database: + +```{autodoc2-object} plone.testing.zodb.stackDemoStorage +render_plugin = "myst" +``` + +### Cleanup + +```{autodoc2-object} plone.app.testing.helpers.tearDownMultiPluginRegistration +render_plugin = "myst" +``` + +```{autodoc2-object} plone.app.testing.cleanup.cleanUpMultiPlugins +render_plugin = "myst" +``` + +## Constants + +Well-known values, safe to depend on in tests and fixtures. +All come from `plone.app.testing`. + +| Constant | Value | Meaning | +| --- | --- | --- | +| `TEST_USER_ID` | `'test_user_1_'` | The default test user's id. | +| `TEST_USER_NAME` | `'test-user'` | The default test user's login name. | +| `TEST_USER_PASSWORD` | `'correct horse battery staple'` | The default test user's password. | +| `TEST_USER_ROLES` | `['Member']` | The default test user's roles. | +| `SITE_OWNER_NAME` | `'admin'` | The site owner (Manager) login name. | +| `SITE_OWNER_PASSWORD` | `'secret'` | The site owner's password. | +| `PLONE_SITE_ID` | `'plone'` | The id of the test Plone site. | +| `PLONE_SITE_TITLE` | `'Plone site'` | The title of the test Plone site. | +| `DEFAULT_LANGUAGE` | `'en'` | The default language of the test site. | +| `ROBOT_TEST_LEVEL` | `5` | The test level at which Robot Framework tests are registered. | + +```{seealso} +- {doc}`write-a-testing-layer` — how to use these to build your `testing.py`. +- {doc}`how-testing-layers-work` — the model beneath these classes. +``` diff --git a/requirements.txt b/requirements.txt index 994eea9cfb..bf119a68e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ sphinxcontrib-youtube sphinxext-opengraph sphinxcontrib.mermaid vale +sphinx-autodoc2 # developer-guide/testing plone.app.testing reference diff --git a/styles/config/vocabularies/Plone/accept.txt b/styles/config/vocabularies/Plone/accept.txt index 88fad90151..b9525bb7d8 100644 --- a/styles/config/vocabularies/Plone/accept.txt +++ b/styles/config/vocabularies/Plone/accept.txt @@ -45,6 +45,11 @@ Plone plone.app.testing plone.meta plone.testing +plone.testing.layer +plone.testing.security +plone.testing.zca +plone.testing.zodb +plone.testing.zope plonecli pluggab(le|ility) pnpm @@ -60,9 +65,11 @@ Razzle RichText Sass Schuko +[Ss]andbox(ed|ing)? subfolder testbrowser toggler +traceback [Tt]owncrier transpilation transpile[drs]{0,1} @@ -81,4 +88,6 @@ webpack wireframe xkcd Zope +zope.testbrowser zope.testrunner +ZServer diff --git a/submodules/plone.app.testing b/submodules/plone.app.testing new file mode 160000 index 0000000000..b9febd454c --- /dev/null +++ b/submodules/plone.app.testing @@ -0,0 +1 @@ +Subproject commit b9febd454cac94d8de84bb418d6ddaacba15889f diff --git a/submodules/plone.testing b/submodules/plone.testing new file mode 160000 index 0000000000..c56d293641 --- /dev/null +++ b/submodules/plone.testing @@ -0,0 +1 @@ +Subproject commit c56d29364146f99b29db30b5ad848ffabfc57471 From fdace45c8144da68c538fdb530ad4951a1bd2996 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Sat, 25 Jul 2026 00:58:21 +0200 Subject: [PATCH 2/4] Bump plone.testing submodule to include the enriched Layer docstring (plone/plone.testing#134) --- submodules/plone.testing | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/plone.testing b/submodules/plone.testing index c56d293641..51e369f763 160000 --- a/submodules/plone.testing +++ b/submodules/plone.testing @@ -1 +1 @@ -Subproject commit c56d29364146f99b29db30b5ad848ffabfc57471 +Subproject commit 51e369f763a6c53e7996c04dc8ee482e73a30b79 From 0184fe67fcd9ca13f180557fa4fb904370752636 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Tue, 28 Jul 2026 15:20:55 +0200 Subject: [PATCH 3/4] Address review from @gforcada on the testing API reference - Sort sphinx-autodoc2 into requirements.txt. - Make the autodoc2 conf.py comments generic instead of naming a specific consumer, which would outdate. - Name the two packages explicitly in the chapter index instead of 'both packages'. - Clarify defaultBases vs the bases argument (class default vs the instantiation-time exception) rather than listing them as equivalent. - Highlight the pytest alternative in the install how-to as a tip. - Name Blicca (formerly Classic UI) and link the Volto testing docs in the test-browser how-to. Kept the reference intro as prose rather than a third admonition, per the Plone docs guidance to use admonitions sparingly. --- docs/conf.py | 8 ++++---- docs/developer-guide/testing/drive-the-test-browser.md | 3 ++- docs/developer-guide/testing/how-testing-layers-work.md | 6 ++++-- docs/developer-guide/testing/install-add-ons-in-tests.md | 2 ++ docs/developer-guide/testing/testing-api-reference.md | 2 +- requirements.txt | 2 +- styles/config/vocabularies/Plone/accept.txt | 1 + 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d56eab0291..19a0c7d4f5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,7 +45,7 @@ extensions = [ "myst_parser", "notfound.extension", - "autodoc2", # developer-guide/testing plone.app.testing reference (static, no import) + "autodoc2", # static API docs from source, no package import needed "sphinx.ext.autodoc", "sphinx.ext.autosummary", # plone.api "sphinx.ext.doctest", # plone.api @@ -302,10 +302,10 @@ # Don't show class signature with the class' name. autodoc_class_signature = "separated" -# -- Options for autodoc2 (plone.app.testing reference) ----------------------- +# -- Options for autodoc2 ----------------------------------------------------- # autodoc2 analyses the source statically, so no package import and no Plone -# installation are needed. It reads the plone.app.testing submodule directly. -# The ``module`` key gives the namespace package its full dotted name. +# installation are needed. It reads the package submodules directly. +# The ``module`` key gives each namespace package its full dotted name. autodoc2_packages = [ { "path": "../submodules/plone.app.testing/src/plone/app/testing", diff --git a/docs/developer-guide/testing/drive-the-test-browser.md b/docs/developer-guide/testing/drive-the-test-browser.md index 406e62b056..774cff5be8 100644 --- a/docs/developer-guide/testing/drive-the-test-browser.md +++ b/docs/developer-guide/testing/drive-the-test-browser.md @@ -15,7 +15,8 @@ This guide shows you how to write an end-to-end functional test that drives Plon ```{important} `zope.testbrowser` runs entirely in Python and does **not** run JavaScript. -Use it for server-rendered pages (Classic UI). To test a Volto frontend, use the Volto documentation's end-to-end testing tools instead. +Use it for server-rendered pages (Blicca, the frontend formerly called Classic UI). +To test a Volto frontend, see [Test add-ons](/volto/development/add-ons/test-add-ons-19) instead. ``` You need a **functional** layer, either `PLONE_FUNCTIONAL_TESTING` or your own layer built with `FunctionalTesting`. diff --git a/docs/developer-guide/testing/how-testing-layers-work.md b/docs/developer-guide/testing/how-testing-layers-work.md index d11116c600..6568f485d0 100644 --- a/docs/developer-guide/testing/how-testing-layers-work.md +++ b/docs/developer-guide/testing/how-testing-layers-work.md @@ -50,8 +50,10 @@ This is how a functional layer can, for example, replace the database with a san ## Layers compose through bases -A layer declares its bases—the layers it builds on—through `defaultBases` or the `bases` argument. -The test runner sets up each base once, in order, before the layer itself, and reuses an already-set-up base rather than building it again. +A layer declares its bases—the layers it builds on. +When you write a reusable layer class, you set them as the `defaultBases` class attribute. +When you instantiate a layer directly to combine existing ones, you pass them as the `bases` argument instead; that is the exception, not the rule. +Either way, the test runner sets up each base once, in order, before the layer itself, and reuses an already-set-up base rather than building it again. The result is a tree of layers, each built once. A typical add-on's stack looks like this: diff --git a/docs/developer-guide/testing/install-add-ons-in-tests.md b/docs/developer-guide/testing/install-add-ons-in-tests.md index 129ee91ffe..6ebd9feeb9 100644 --- a/docs/developer-guide/testing/install-add-ons-in-tests.md +++ b/docs/developer-guide/testing/install-add-ons-in-tests.md @@ -16,7 +16,9 @@ This guide shows you how to install a GenericSetup profile or an add-on inside a It uses the helpers from {doc}`testing-api-reference`. The examples assume a layer whose fixture already loaded your add-on's ZCML—see {doc}`write-a-testing-layer`. +```{tip} If you use pytest, {doc}`pytest-plone ` also offers an `installer` fixture and an `@pytest.mark.portal(profiles=[...])` marker that do the same thing with less boilerplate. +``` ## Apply a profile diff --git a/docs/developer-guide/testing/testing-api-reference.md b/docs/developer-guide/testing/testing-api-reference.md index b6090d8f72..c847c6f3e7 100644 --- a/docs/developer-guide/testing/testing-api-reference.md +++ b/docs/developer-guide/testing/testing-api-reference.md @@ -17,7 +17,7 @@ Technical reference for Plone's testing API, drawn from two packages: - [plone.testing](https://github.com/plone/plone.testing): the underlying layer model and the Zope-level tools. You import from whichever package a symbol lives in, but from a test author's point of view they are one toolkit, so this page is organized by task rather than by package. -Each symbol is documented at its canonical location; re-exports are noted. +Each symbol is documented at its canonical location; where one package re-exports a symbol from the other, that is noted. To use these to write a `testing.py`, see {doc}`write-a-testing-layer`. For the model behind them, see {doc}`how-testing-layers-work`. diff --git a/requirements.txt b/requirements.txt index bf119a68e8..75ec0c9411 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ linkify-it-py plone-sphinx-theme<2 sphinx<9 # See https://github.com/plone/documentation/issues/2037 sphinx-autobuild +sphinx-autodoc2 sphinx-copybutton sphinx-design # Documentation only sphinx-examples @@ -17,4 +18,3 @@ sphinxcontrib-youtube sphinxext-opengraph sphinxcontrib.mermaid vale -sphinx-autodoc2 # developer-guide/testing plone.app.testing reference diff --git a/styles/config/vocabularies/Plone/accept.txt b/styles/config/vocabularies/Plone/accept.txt index b9525bb7d8..20b4cedfcc 100644 --- a/styles/config/vocabularies/Plone/accept.txt +++ b/styles/config/vocabularies/Plone/accept.txt @@ -9,6 +9,7 @@ APIs backport(ed|ing) Barceloneta [Bb]oolean +Blicca bugfix [Bb]uildout cacheable From 5c1529834c74fc24e669d0dde4fb0e6c0cee1b30 Mon Sep 17 00:00:00 2001 From: "Jens W. Klein" Date: Wed, 29 Jul 2026 11:44:37 +0200 Subject: [PATCH 4/4] Actually name the two packages in the chapter index The previous review fix silently no-op'd (the search text omitted a word), so the line still read 'from both packages'. Name plone.app.testing and plone.testing explicitly, as intended. --- docs/developer-guide/testing/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer-guide/testing/index.md b/docs/developer-guide/testing/index.md index e382b8d356..6d2afc9ad6 100644 --- a/docs/developer-guide/testing/index.md +++ b/docs/developer-guide/testing/index.md @@ -59,7 +59,7 @@ Do not use both in one package. ## Reference and background -- {doc}`testing-api-reference`: the layers, fixtures, helpers, and sandboxing, from both packages. +- {doc}`testing-api-reference`: the layers, fixtures, helpers, and sandboxing, across `plone.app.testing` and `plone.testing`. - {doc}`how-testing-layers-work`: the layer model beneath it all. ```{toctree}