-
-
Notifications
You must be signed in to change notification settings - Fork 189
Add a Testing API reference (plone.app.testing + plone.testing) #2097
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jensens
wants to merge
5
commits into
backend-testing-docs
Choose a base branch
from
plone-app-testing-docs
base: backend-testing-docs
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ab4f779
Add a Testing API reference covering plone.app.testing and plone.testing
jensens fdace45
Bump plone.testing submodule to include the enriched Layer docstring …
jensens 9e3592c
Merge branch 'backend-testing-docs' into plone-app-testing-docs
jensens 0184fe6
Address review from @gforcada on the testing API reference
jensens 5c15298
Actually name the two packages in the chapter index
jensens File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| --- | ||
| 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 (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`. | ||
| 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. | ||
| ``` |
108 changes: 108 additions & 0 deletions
108
docs/developer-guide/testing/how-testing-layers-work.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| --- | ||
| 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. | ||
| 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: | ||
|
|
||
| ```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 <testing-api-reference>`; 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). | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| --- | ||
| 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`. | ||
|
|
||
| ```{tip} | ||
| If you use pytest, {doc}`pytest-plone </developer-guide/testing/pytest>` 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`. | ||
| ``` | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.