[Testbot] Plone 5.0 - Python 2.7 - Build # 2084 - Still failing! - 0 failure(s)
jenkins at plone.org
jenkins at plone.org
Sun Mar 30 23:03:13 UTC 2014
-------------------------------------------------------------------------------
Plone 5.0 - Python 2.7 - Build # 2084 - Still Failing!
-------------------------------------------------------------------------------
http://jenkins.plone.org/job/plone-5.0-python-2.7/2084/
-------------------------------------------------------------------------------
CHANGES
-------------------------------------------------------------------------------
Repository: plone.app.testing
Branch: refs/heads/master
Date: 2014-03-30T23:01:17+02:00
Author: Paul Roeland (polyester) <paula at polyester.org>
Commit: https://github.com/plone/plone.app.testing/commit/e1bddb6f5e045350c38374ce91b1771531aaf391
move README.rst to /docs/source folder
Files changed:
A README.rst
A docs/source/README.rst
M setup.py
D README.rst
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 94d8b58..0000000
--- a/README.rst
+++ /dev/null
@@ -1,1491 +0,0 @@
-Introduction
-============
-
-.. contents:: Table of contents
-
-``plone.app.testing`` provides tools for writing integration and functional
-tests for code that runs on top of Plone. It is based on `plone.testing`_.
-If you are unfamiliar with ``plone.testing``, the concept of layers, or the
-`zope.testing`_ testrunner, please take a look at the the ``plone.testing``
-documentation. In fact, even if you are working exclusively with Plone, you
-are likely to want to use some of its features for unit testing.
-
-In short, ``plone.app.testing`` includes:
-
-* A set of layers that set up fixtures containing a Plone site, intended for
- writing integration and functional tests.
-* A collection of helper functions, some useful for writing your own layers
- and some applicable to tests themselves.
-* A convenient layer base class, extending ``plone.testing.Layer``, which
- makes it easier to write custom layers extending the Plone site fixture,
- with proper isolation and tear-down.
-* Cleanup hooks for ``zope.testing.cleanup`` to clean up global state found
- in a Plone installation. This is useful for unit testing.
-
-Compatibility
--------------
-
-``plone.app.testing`` 4.x works with Plone 4 and Zope 2.12. It may work with
-newer versions. It will not work with earlier versions. Use
-``plone.app.testing`` 3.x for Plone 3 and Zope 2.10.
-
-Installation and usage
-======================
-
-To use ``plone.app.testing`` in your own package, you need to add it as a
-dependency. Most people prefer to keep test-only dependencies separate, so
-that they do not need to be installed in scenarios (such as on a production
-server) where the tests will not be run. This can be achieved using a
-``test`` extra.
-
-In ``setup.py``, add or modify the ``extras_require`` option, like so::
-
- extras_require = {
- 'test': [
- 'plone.app.testing',
- ]
- },
-
-This will also include ``plone.testing``, with the ``[z2]``, ``[zca]`` and
-``[zodb]`` extras (which ``plone.app.testing`` itself relies on).
-
-Please see the `plone.testing`_ documentation for more details about how to
-add a test runner to your buildout, and how to write and run tests.
-
-Layer reference
-===============
-
-This package contains a layer class,
-``plone.app.testing.layers.PloneFixture``, which sets up a Plone site fixture.
-It is combined with other layers from `plone.testing`_ to provide a number of
-layer instances. It is important to realise that these layers all have the
-same fundamental fixture: they just manage test setup and tear-down
-differently.
-
-When set up, the fixture will:
-
-* Create a ZODB sandbox, via a stacked ``DemoStorage``. This ensures
- persistent changes made during layer setup can be cleanly torn down.
-* Configure a global component registry sandbox. This ensures that global
- component registrations (e.g. as a result of loading ZCML configuration)
- can be cleanly torn down.
-* Create a configuration context with the ``disable-autoinclude`` feature
- set. This has the effect of stopping Plone from automatically loading the
- configuration of any installed package that uses the
- ``z3c.autoinclude.plugin:plone`` entry point via `z3c.autoinclude`_. (This
- is to avoid accidentally polluting the test fixture - custom layers should
- load packages' ZCML configuration explicitly if required).
-* Install a number of Zope 2-style products on which Plone depends.
-* Load the ZCML for these products, and for ``Products.CMFPlone``, which in
- turn pulls in the configuration for the core of Plone.
-* Create a default Plone site, with the default theme enabled, but with no
- default content.
-* Add a user to the root user folder with the ``Manager`` role.
-* Add a test user to this instance with the ``Member`` role.
-
-For each test:
-
-* The test user is logged in
-* The local component site is set
-* Various global caches are cleaned up
-
-Various constants in the module ``plone.app.testing.interfaces`` are defined
-to describe this environment:
-
-+----------------------+--------------------------------------------------+
-| **Constant** | **Purpose** |
-+----------------------+--------------------------------------------------+
-| PLONE_SITE_ID | The id of the Plone site object inside the Zope |
-| | application root. |
-+----------------------+--------------------------------------------------+
-| PLONE_SITE_TITLE | The title of the Plone site |
-+----------------------+--------------------------------------------------+
-| DEFAULT_LANGUAGE | The default language of the Plone site ('en') |
-+----------------------+--------------------------------------------------+
-| TEST_USER_ID | The id of the test user |
-+----------------------+--------------------------------------------------+
-| TEST_USER_NAME | The username of the test user |
-+----------------------+--------------------------------------------------+
-| TEST_USER_PASSWORD | The password of the test user |
-+----------------------+--------------------------------------------------+
-| TEST_USER_ROLES | The default global roles of the test user - |
-| | ('Member',) |
-+----------------------+--------------------------------------------------+
-| SITE_OWNER_NAME | The username of the user owning the Plone site. |
-+----------------------+--------------------------------------------------+
-| SITE_OWNER_PASSWORD | The password of the user owning the Plone site. |
-+----------------------+--------------------------------------------------+
-
-All the layers also expose a resource in addition to those from their
-base layers, made available during tests:
-
-``portal``
- The Plone site root.
-
-Plone site fixture
-------------------
-
-+------------+--------------------------------------------------+
-| Layer: | ``plone.app.testing.PLONE_FIXTURE`` |
-+------------+--------------------------------------------------+
-| Class: | ``plone.app.testing.layers.PloneFixture`` |
-+------------+--------------------------------------------------+
-| Bases: | ``plone.testing.z2.STARTUP`` |
-+------------+--------------------------------------------------+
-| Resources: | |
-+------------+--------------------------------------------------+
-
-This layer sets up the Plone site fixture on top of the ``z2.STARTUP``
-fixture.
-
-You should not use this layer directly, as it does not provide any test
-lifecycle or transaction management. Instead, you should use a layer
-created with either the ``IntegrationTesting`` or ``FunctionalTesting``
-classes, as outlined below.
-
-Integration and functional testing test lifecycles
---------------------------------------------------
-
-``plone.app.testing`` comes with two layer classes, ``IntegrationTesting``
-and ``FunctionalTesting``, which derive from the corresponding layer classes
-in ``plone.testing.z2``.
-
-These classes set up the ``app``, ``request`` and ``portal`` resources, and
-reset the fixture (including various global caches) between each test run.
-
-As with the classes in ``plone.testing``, the ``IntegrationTesting`` class
-will create a new transaction for each test and roll it back on test tear-
-down, which is efficient for integration testing, whilst ``FunctionalTesting``
-will create a stacked ``DemoStorage`` for each test and pop it on test tear-
-down, making it possible to exercise code that performs an explicit commit
-(e.g. via tests that use ``zope.testbrowser``).
-
-When creating a custom fixture, the usual pattern is to create a new layer
-class that has ``PLONE_FIXTURE`` as its default base, instantiating that as a
-separate "fixture" layer. This layer is not to be used in tests directly,
-since it won't have test/transaction lifecycle management, but represents a
-shared fixture, potentially for both functional and integration testing. It
-is also the point of extension for other layers that follow the same pattern.
-
-Once this fixture has been defined, "end-user" layers can be defined using
-the ``IntegrationTesting`` and ``FunctionalTesting`` classes. For example::
-
- from plone.testing import Layer
- from plone.app.testing import PLONE_FIXTURE
- from plone.app.testing import IntegrationTesting, FunctionalTesting
-
- class MyFixture(Layer):
- defaultBases = (PLONE_FIXTURE,)
-
- ...
-
- MY_FIXTURE = MyFixture()
-
- MY_INTEGRATION_TESTING = IntegrationTesting(bases=(MY_FIXTURE,), name="MyFixture:Integration")
- MY_FUNCTIONAL_TESTING = FunctionalTesting(bases=(MY_FIXTURE,), name="MyFixture:Functional")
-
-See the ``PloneSandboxLayer`` layer below for a more comprehensive example.
-
-Plone integration testing
--------------------------
-
-+------------+--------------------------------------------------+
-| Layer: | ``plone.app.testing.PLONE_INTEGRATION_TESTING`` |
-+------------+--------------------------------------------------+
-| Class: | ``plone.app.testing.layers.IntegrationTesting`` |
-+------------+--------------------------------------------------+
-| Bases: | ``plone.app.testing.PLONE_FIXTURE`` |
-+------------+--------------------------------------------------+
-| Resources: | ``portal`` (test setup only) |
-+------------+--------------------------------------------------+
-
-This layer can be used for integration testing against the basic
-``PLONE_FIXTURE`` layer.
-
-You can use this directly in your tests if you do not need to set up any
-other shared fixture.
-
-However, you would normally not extend this layer - see above.
-
-
-Plone functional testing
-------------------------
-
-+------------+--------------------------------------------------+
-| Layer: | ``plone.app.testing.PLONE_FUNCTIONAL_TESTING`` |
-+------------+--------------------------------------------------+
-| Class: | ``plone.app.testing.layers.FunctionalTesting`` |
-+------------+--------------------------------------------------+
-| Bases: | ``plone.app.testing.PLONE_FIXTURE`` |
-+------------+--------------------------------------------------+
-| Resources: | ``portal`` (test setup only) |
-+------------+--------------------------------------------------+
-
-This layer can be used for functional testing against the basic
-``PLONE_FIXTURE`` layer, for example using ``zope.testbrowser``.
-
-You can use this directly in your tests if you do not need to set up any
-other shared fixture.
-
-Again, you would normally not extend this layer - see above.
-
-Plone ZServer
--------------
-
-+------------+--------------------------------------------------+
-| Layer: | ``plone.app.testing.PLONE_ZSERVER`` |
-+------------+--------------------------------------------------+
-| Class: | ``plone.testing.z2.ZServer`` |
-+------------+--------------------------------------------------+
-| Bases: | ``plone.app.testing.PLONE_FUNCTIONAL_TESTING`` |
-+------------+--------------------------------------------------+
-| Resources: | ``portal`` (test setup only) |
-+------------+--------------------------------------------------+
-
-This is layer is intended for functional testing using a live, running HTTP
-server, e.g. using Selenium or Windmill.
-
-Again, you would not normally extend this layer. To create a custom layer
-that has a running ZServer, you can use the same pattern as this one, e.g.::
-
- from plone.testing import Layer
- from plone.testing import z2
- from plone.app.testing import PLONE_FIXTURE
- from plone.app.testing import FunctionalTesting
-
- class MyFixture(Layer):
- defaultBases = (PLONE_FIXTURE,)
-
- ...
-
- MY_FIXTURE = MyFixture()
- MY_ZSERVER = FunctionalTesting(bases=(MY_FIXTURE, z2.ZSERVER_FIXTURE), name='MyFixture:ZServer')
-
-See the description of the ``z2.ZSERVER`` layer in `plone.testing`_
-for further details.
-
-Plone FTP server
-----------------
-
-+------------+--------------------------------------------------+
-| Layer: | ``plone.app.testing.PLONE_FTP_SERVER`` |
-+------------+--------------------------------------------------+
-| Class: | ``plone.app.testing.layers.FunctionalTesting`` |
-+------------+--------------------------------------------------+
-| Bases: | ``plone.app.testing.PLONE_FIXTURE`` |
-| | ``plone.testing.z2.ZSERVER_FIXTURE`` |
-+------------+--------------------------------------------------+
-| Resources: | ``portal`` (test setup only) |
-+------------+--------------------------------------------------+
-
-This is layer is intended for functional testing using a live FTP server.
-
-It is semantically equivalent to the ``PLONE_ZSERVER`` layer.
-
-See the description of the ``z2.FTP_SERVER`` layer in `plone.testing`_
-for further details.
-
-Helper functions
-================
-
-A number of helper functions are provided for use in tests and custom layers.
-
-Plone site context manager
---------------------------
-
-``ploneSite(db=None, connection=None, environ=None)``
- Use this context manager to access and make changes to the Plone site
- during layer setup. In most cases, you will use it without arguments,
- but if you have special needs, you can tie it to a particular database
- instance. See the description of the ``zopeApp()`` context manager in
- `plone.testing`_ (which this context manager uses internally) for details.
-
- The usual pattern is to call it during ``setUp()`` or ``tearDown()`` in
- your own layers::
-
- from plone.testing import Layer
- from plone.app.testing import ploneSite
-
- class MyLayer(Layer):
-
- def setUp(self):
-
- ...
-
- with ploneSite() as portal:
-
- # perform operations on the portal, e.g.
- portal.title = u"New title"
-
- Here, ``portal`` is the Plone site root. A transaction is begun before
- entering the ``with`` block, and will be committed upon exiting the block,
- unless an exception is raised, in which case it will be rolled back.
-
- Inside the block, the local component site is set to the Plone site root,
- so that local component lookups should work.
-
- **Warning:** Do not attempt to load ZCML files inside a ``ploneSite``
- block. Because the local site is set to the Plone site, you may end up
- accidentally registering components in the local site manager, which can
- cause pickling errors later.
-
- **Note:** You should not use this in a test, or in a ``testSetUp()`` or
- ``testTearDown()`` method of a layer based on one of the layer in this
- package. Use the ``portal`` resource instead.
-
- **Also note:** If you are writing a layer setting up a Plone site fixture,
- you may want to use the ``PloneSandboxLayer`` layer base class, and
- implement the ``setUpZope()``, ``setUpPloneSite()``, ``tearDownZope()``
- and/or ``tearDownPloneSite()`` methods instead. See below.
-
-User management
----------------
-
-``login(portal, userName)``
- Simulate login as the given user. This is based on the ``z2.login()``
- helper in `plone.testing`_, but instead of passing a specific user folder,
- you pass the portal (e.g. as obtained via the ``portal`` layer resource).
-
- For example::
-
- import unittest2 as unittest
-
- from plone.app.testing import PLONE_INTEGRATION_TESTING
- from plone.app.testing import TEST_USER_NAME
- from plone.app.testing import login
-
- ...
-
- class MyTest(unittest.TestCase):
-
- layer = PLONE_INTEGRATION_TESTING
-
- def test_something(self):
- portal = self.layer['portal']
- login(portal, TEST_USER_NAME)
-
- ...
-
-``logout()``
- Simulate logging out, i.e. becoming the anonymous user. This is equivalent
- to the ``z2.logout()`` helper in `plone.testing`_.
-
- For example::
-
- import unittest2 as unittest
-
- from plone.app.testing import PLONE_INTEGRATION_TESTING
- from plone.app.testing import logout
-
- ...
-
- class MyTest(unittest.TestCase):
-
- layer = PLONE_INTEGRATION_TESTING
-
- def test_something(self):
- portal = self.layer['portal']
- logout()
-
- ...
-
-``setRoles(portal, userId, roles)``
- Set the roles for the given user. ``roles`` is a list of roles.
-
- For example::
-
- import unittest2 as unittest
-
- from plone.app.testing import PLONE_INTEGRATION_TESTING
- from plone.app.testing import TEST_USER_ID
- from plone.app.testing import setRoles
-
- ...
-
- class MyTest(unittest.TestCase):
-
- layer = PLONE_INTEGRATION_TESTING
-
- def test_something(self):
- portal = self.layer['portal']
- setRoles(portal, TEST_USER_ID, ['Manager'])
-
-Product and profile installation
---------------------------------
-
-``applyProfile(portal, profileName)``
- Install a GenericSetup profile (usually an extension profile) by name,
- using the ``portal_setup`` tool. The name is normally made up of a package
- name and a profile name. Do not use the ``profile-`` prefix.
-
- For example::
-
- from plone.testing import Layer
-
- from plone.app.testing import ploneSite
- from plone.app.testing import applyProfile
-
- ...
-
- class MyLayer(Layer):
-
- ...
-
- def setUp(self):
-
- ...
-
- with ploneSite() as portal:
- applyProfile(portal, 'my.product:default')
-
- ...
-
-``quickInstallProduct(portal, productName, reinstall=False)``
- Use this function to install a particular product into the given Plone
- site, using the ``portal_quickinstaller`` tool. If ``reinstall`` is
- ``False`` and the product is already installed, nothing will happen; if
- ``reinstall`` is ``True``, the product will be reinstalled. The
- ``productName`` should be a full dotted name, e.g. ``Products.MyProduct``,
- or ``my.product``.
-
- For example::
-
- from plone.testing import Layer
-
- from plone.app.testing import ploneSite
- from plone.app.testing import quickInstallProduct
-
- ...
-
- class MyLayer(Layer):
-
- ...
-
- def setUp(self):
-
- ...
-
- with ploneSite() as portal:
- quickInstallProduct(portal, 'my.product')
-
- ...
-
-Component architecture sandboxing
----------------------------------
-
-``pushGlobalRegistry(portal, new=None, name=None)``
- Create or obtain a stack of global component registries, and push a new
- registry to the top of the stack. This allows Zope Component Architecture
- registrations (e.g. loaded via ZCML) to be effectively torn down.
-
- If you are going to use this function, please read the corresponding
- documentation for ``zca.pushGlobalRegistry()`` in `plone.testing`_. In
- particular, note that you *must* reciprocally call ``popGlobalRegistry()``
- (see below).
-
- This helper is based on ``zca.pushGlobalRegistry()``, but will also fix
- up the local component registry in the Plone site ``portal`` so that it
- has the correct bases.
-
- For example::
-
- from plone.testing import Layer
-
- from plone.app.testing import ploneSite
- from plone.app.testing import pushGlobalRegistry
- from plone.app.testing import popGlobalRegistry
-
- ...
-
- class MyLayer(Layer):
-
- ...
-
- def setUp(self):
-
- ...
-
- with ploneSite() as portal:
- pushGlobalRegistry(portal)
-
- ...
-
-``popGlobalRegistry(portal)``
- Tear down the top of the component architecture stack, as created with
- ``pushGlobalRegistry()``
-
- For example::
-
- ...
-
- def tearDown(self):
-
- with ploneSite() as portal:
- popGlobalRegistry(portal)
-
-Global state cleanup
---------------------
-
-``tearDownMultiPluginRegistration(pluginName)``
- PluggableAuthService "MultiPlugins" are kept in a global registry. If
- you have registered a plugin, e.g. using the ``registerMultiPlugin()``
- API, you should tear that registration down in your layer's ``tearDown()``
- method. You can use this helper, passing a plugin name.
-
- For example::
-
- from plone.testing import Layer
-
- from plone.app.testing import ploneSite
- from plone.app.testing import tearDownMultiPluginRegistration
-
- ...
-
- class MyLayer(Layer):
-
- ...
-
- def tearDown(self):
-
- tearDownMultiPluginRegistration('MyPlugin')
-
- ...
-
-Layer base class
-================
-
-If you are writing a custom layer to test your own Plone add-on product, you
-will often want to do the following on setup:
-
-1. Stack a new ``DemoStorage`` on top of the one from the base layer. This
- ensures that any persistent changes performed during layer setup can be
- torn down completely, simply by popping the demo storage.
-
-2. Stack a new ZCML configuration context. This keeps separate the information
- about which ZCML files were loaded, in case other, independent layers want
- to load those same files after this layer has been torn down.
-
-3. Push a new global component registry. This allows you to register
- components (e.g. by loading ZCML or using the test API from
- ``zope.component``) and tear down those registration easily by popping the
- component registry.
-
-4. Load your product's ZCML configuration
-
-5. Install the product into the test fixture Plone site
-
-Of course, you may wish to make other changes too, such as creating some base
-content or changing some settings.
-
-On tear-down, you will then want to:
-
-1. Remove any Pluggable Authentication Service "multi-plugins" that were added
- to the global registry during setup.
-
-2. Pop the global component registry to unregister components loaded via ZCML.
-
-3. Pop the configuration context resource to restore its state.
-
-4. Pop the ``DemoStorage`` to undo any persistent changes.
-
-If you have made other changes on setup that are not covered by this broad
-tear-down, you'll also want to tear those down explicitly here.
-
-Stacking a demo storage and component registry is the safest way to avoid
-fixtures bleeding between tests. However, it can be tricky to ensure that
-everything happens in the right order.
-
-To make things easier, you can use the ``PloneSandboxLayer`` layer base class.
-This extends ``plone.testing.Layer`` and implements ``setUp()`` and
-``tearDown()`` for you. You simply have to override one or more of the
-following methods:
-
-``setUpZope(self, app, configurationContext)``
- This is called during setup. ``app`` is the Zope application root.
- ``configurationContext`` is a newly stacked ZCML configuration context.
- Use this to load ZCML, install products using the helper
- ``plone.testing.z2.installProduct()``, or manipulate other global state.
-
-``setUpPloneSite(self, portal)``
- This is called during setup. ``portal`` is the Plone site root as
- configured by the ``ploneSite()`` context manager. Use this to make
- persistent changes inside the Plone site, such as installing products
- using the ``applyProfile()`` or ``quickInstallProduct()`` helpers, or
- setting up default content.
-
-``tearDownZope(self, app)``
- This is called during tear-down, before the global component registry and
- stacked ``DemoStorage`` are popped. Use this to tear down any additional
- global state.
-
- **Note:** Global component registrations PAS multi-plugin registrations are
- automatically torn down. Product installations are not, so you should use
- the ``uninstallProduct()`` helper if any products were installed during
- ``setUpZope()``.
-
-``tearDownPloneSite(self, portal)``
- This is called during tear-down, before the global component registry and
- stacked ``DemoStorage`` are popped. During this method, the local
- component site hook is set, giving you access to local components.
-
- **Note:** Persistent changes to the ZODB are automatically torn down by
- virtue of a stacked ``DemoStorage``. Thus, this method is less commonly
- used than the others described here.
-
-Let's show a more comprehensive example of what such a layer may look like.
-Imagine we have a product ``my.product``. It has a ``configure.zcml`` file
-that loads some components and registers a ``GenericSetup`` profile, making it
-installable in the Plone site. On layer setup, we want to load the product's
-configuration and install it into the Plone site.
-
-The layer would conventionally live in a module ``testing.py`` at the root of
-the package, i.e. ``my.product.testing``::
-
- from plone.app.testing import PloneSandboxLayer
- from plone.app.testing import PLONE_FIXTURE
- from plone.app.testing import IntegrationTesting
-
- from plone.testing import z2
-
- class MyProduct(PloneSandboxLayer):
-
- defaultBases = (PLONE_FIXTURE,)
-
- def setUpZope(self, app, configurationContext):
- # Load ZCML
- import my.product
- self.loadZCML(package=my.product)
-
- # Install product and call its initialize() function
- z2.installProduct(app, 'my.product')
-
- # Note: you can skip this if my.product is not a Zope 2-style
- # product, i.e. it is not in the Products.* namespace and it
- # does not have a <five:registerPackage /> directive in its
- # configure.zcml.
-
- def setUpPloneSite(self, portal):
- # Install into Plone site using portal_setup
- self.applyProfile(portal, 'my.product:default')
-
- def tearDownZope(self, app):
- # Uninstall product
- z2.uninstallProduct(app, 'my.product')
-
- # Note: Again, you can skip this if my.product is not a Zope 2-
- # style product
-
- MY_PRODUCT_FIXTURE = MyProduct()
- MY_PRODUCT_INTEGRATION_TESTING = IntegrationTesting(bases=(MY_PRODUCT_FIXTURE,), name="MyProduct:Integration")
-
-Here, ``MY_PRODUCT_FIXTURE`` is the "fixture" base layer. Other layers can
-use this as a base if they want to build on this fixture, but it would not
-be used in tests directly. For that, we have crated an ``IntegrationTesting``
-instance, ``MY_PRODUCT_INTEGRATION_TESTING``.
-
-Of course, we could have created a ``FunctionalTesting`` instance as
-well, e.g.::
-
- MY_PRODUCT_FUNCTIONAL_TESTING = FunctionalTesting(bases=(MY_PRODUCT_FIXTURE,), name="MyProduct:Functional")
-
-Of course, we could do a lot more in the layer setup. For example, let's say
-the product had a content type 'my.product.page' and we wanted to create some
-test content. We could do that with::
-
- from plone.app.testing import TEST_USER_ID
- from plone.app.testing import TEST_USER_NAME
- from plone.app.testing import login
- from plone.app.testing import setRoles
-
- ...
-
- def setUpPloneSite(self, portal):
-
- ...
-
- setRoles(portal, TEST_USER_ID, ['Manager'])
- login(portal, TEST_USER_NAME)
- portal.invokeFactory('my.product.page', 'page-1', title=u"Page 1")
- setRoles(portal, TEST_USER_ID, ['Member'])
-
- ...
-
-Note that unlike in a test, there is no user logged in at layer setup time,
-so we have to explicitly log in as the test user. Here, we also grant the test
-user the ``Manager`` role temporarily, to allow object construction (which
-performs an explicit permission check).
-
- **Note:** Automatic tear down suffices for all the test setup above. If
- the only changes made during layer setup are to persistent, in-ZODB data,
- or the global component registry then no additional tear-down is required.
- For any other global state being managed, you should write a
- ``tearDownPloneSite()`` method to perform the necessary cleanup.
-
-Given this layer, we could write a test (e.g. in ``tests.py``) like::
-
- import unittest2 as unittest
- from my.product.testing import MY_PRODUCT_INTEGRATION_TESTING
-
- class IntegrationTest(unittest.TestCase):
-
- layer = MY_PRODUCT_INTEGRATION_TESTING
-
- def test_page_dublin_core_title(self):
- portal = self.layer['portal']
-
- page1 = portal['page-1']
- page1.title = u"Some title"
-
- self.assertEqual(page1.Title(), u"Some title")
-
-Please see `plone.testing`_ for more information about how to write and run
-tests and assertions.
-
-Common test patterns
-====================
-
-`plone.testing`_'s documentation contains details about the fundamental
-techniques for writing tests of various kinds. In a Plone context, however,
-some patterns tend to crop up time and again. Below, we will attempt to
-catalogue some of the more commonly used patterns via short code samples.
-
-The examples in this section are all intended to be used in tests. Some may
-also be useful in layer set-up/tear-down. We have used ``unittest`` syntax
-here, although most of these examples could equally be adopted to doctests.
-
-We will assume that you are using a layer that has ``PLONE_FIXTURE`` as a base
-(whether directly or indirectly) and uses the ``IntegrationTesting`` or
-``FunctionalTesting`` classes as shown above.
-
-We will also assume that the variables ``app``, ``portal`` and ``request`` are
-defined from the relative layer resources, e.g. with::
-
- app = self.layer['app']
- portal = self.layer['portal']
- request = self.layer['request']
-
-Note that in a doctest set up using the ``layered()`` function from
-``plone.testing``, ``layer`` is in the global namespace, so you would do e.g.
-``portal = layer['portal']``.
-
-Where imports are required, they are shown alongside the code example. If
-a given import or variable is used more than once in the same section, it
-will only be shown once.
-
-Basic content management
-------------------------
-
-To create a content item of type 'Folder' with the id 'f1' in the root of
-the portal::
-
- portal.invokeFactory('Folder', 'f1', title=u"Folder 1")
-
-The ``title`` argument is optional. Other basic properties, like
-``description``, can be set as well.
-
-Note that this may fail with an ``Unauthorized`` exception, since the test
-user won't normally have permissions to add content in the portal root, and
-the ``invokeFactory()`` method performs an explicit security check. You can
-set the roles of the test user to ensure that he has the necessary
-permissions::
-
- from plone.app.testing import setRoles
- from plone.app.testing import TEST_USER_ID
-
- setRoles(portal, TEST_USER_ID, ['Manager'])
- portal.invokeFactory('Folder', 'f1', title=u"Folder 1")
-
-To obtain this object, acquisition-wrapped in its parent::
-
- f1 = portal['f1']
-
-To make an assertion against an attribute or method of this object::
-
- self.assertEqual(f1.Title(), u"Folder 1")
-
-To modify the object::
-
- f1.setTitle(u"Some title")
-
-To add another item inside the folder f1::
-
- f1.invokeFactory('Document', 'd1', title=u"Document 1")
- d1 = f1['d1']
-
-To check if an object is in a container::
-
- self.assertTrue('f1' in portal)
-
-To delete an object from a container:
-
- del portal['f1']
-
-There is no content or workflows installed by default. You can enable workflows::
-
- portal.portal_workflow.setDefaultChain("simple_publication_workflow")
-
-Searching
----------
-
-To obtain the ``portal_catalog`` tool::
-
- from Products.CMFCore.utils import getToolByName
-
- catalog = getToolByName(portal, 'portal_catalog')
-
-To search the catalog::
-
- results = catalog(portal_type="Document")
-
-Keyword arguments are search parameters. The result is a lazy list. You can
-call ``len()`` on it to get the number of search results, or iterate through
-it. The items in the list are catalog brains. They have attributes that
-correspond to the "metadata" columns configured for the catalog, e.g.
-``Title``, ``Description``, etc. Note that these are simple attributes (not
-methods), and contain the value of the corresponding attribute or method from
-the source object at the time the object was cataloged (i.e. they are not
-necessarily up to date).
-
-To make assertions against the search results::
-
- self.assertEqual(len(results), 1)
-
- # Copy the list into memory so that we can use [] notation
- results = list(results)
-
- # Check the first (and in this case only) result in the list
- self.assertEqual(results[0].Title, u"Document 1")
-
-To get the path of a given item in the search results::
-
- self.assertEqual(resuls[0].getPath(), portal.absolute_url_path() + '/f1/d1')
-
-To get an absolute URL::
-
- self.assertEqual(resuls[0].getURL(), portal.absolute_url() + '/f1/d1')
-
-To get the original object::
-
- obj = results[0].getObject()
-
-To re-index an object d1 so that its catalog information is up to date::
-
- d1.reindexObject()
-
-User management
----------------
-
-To create a new user::
-
- from Products.CMFCore.utils import getToolByName
-
- acl_users = getToolByName(portal, 'acl_users')
-
- acl_users.userFolderAddUser('user1', 'secret', ['Member'], [])
-
-The arguments are the username (which will also be the user id), the password,
-a list of roles, and a list of domains (rarely used).
-
-To make a particular user active ("logged in") in the integration testing
-environment use the ``login`` method and pass it the username::
-
- from plone.app.testing import login
-
- login(portal, 'user1')
-
-To log out (become anonymous)::
-
- from plone.app.testing import logout
-
- logout()
-
-To obtain the current user::
-
- from AccessControl import getSecurityManager
-
- user = getSecurityManager().getUser()
-
-To obtain a user by name::
-
- user = acl_users.getUser('user1')
-
-Or by user id (id and username are often the same, but can differ in real-world
-scenarios)::
-
- user = acl_users.getUserById('user1')
-
-To get the user's user name::
-
- userName = user.getUserName()
-
-To get the user's id::
-
- userId = user.getId()
-
-Permissions and roles
----------------------
-
-To get a user's roles in a particular context (taking local roles into
-account)::
-
- from AccessControl import getSecurityManager
-
- user = getSecurityManager().getUser()
-
- self.assertEqual(user.getRolesInContext(portal), ['Member'])
-
-To change the test user's roles::
-
- from plone.app.testing import setRoles
- from plone.app.testing import TEST_USER_ID
-
- setRoles(portal, TEST_USER_ID, ['Member', 'Manager'])
-
-Pass a different user name to change the roles of another user.
-
-To grant local roles to a user in the folder f1::
-
- f1.manage_setLocalRoles(TEST_USER_ID, ('Reviewer',))
-
-To check the local roles of a given user in the folder 'f1'::
-
- self.assertEqual(f1.get_local_roles_for_userid(TEST_USER_ID), ('Reviewer',))
-
-To grant the 'View' permission to the roles 'Member' and 'Manager' in the
-portal root without acquiring additional roles from its parents::
-
- portal.manage_permission('View', ['Member', 'Manager'], acquire=False)
-
-This method can also be invoked on a folder or individual content item.
-
-To assert which roles have the permission 'View' in the context of the
-portal::
-
- roles = [r['name'] for r in portal.rolesOfPermission('View') if r['selected']]
- self.assertEqual(roles, ['Member', 'Manager'])
-
-To assert which permissions have been granted to the 'Reviewer' role in the
-context of the portal::
-
- permissions = [p['name'] for p in portal.permissionsOfRole('Reviewer') if p['selected']]
- self.assertTrue('Review portal content' in permissions)
-
-To add a new role::
-
- portal._addRole('Tester')
-
-This can now be assigned to users globally (using the ``setRoles`` helper)
-or locally (using ``manage_setLocalRoles()``).
-
-To assert which roles are available in a given context::
-
- self.assertTrue('Tester' in portal.valid_roles())
-
-Workflow
---------
-
-To set the default workflow chain::
-
- from Products.CMFCore.utils import getToolByName
-
- workflowTool = getToolByName(portal, 'portal_workflow')
-
- workflowTool.setDefaultChain('my_workflow')
-
-In Plone, most chains contain only one workflow, but the ``portal_workflow``
-tool supports longer chains, where an item is subject to more than one
-workflow simultaneously.
-
-To set a multi-workflow chain, separate workflow names by commas.
-
-To get the default workflow chain::
-
- self.assertEqual(workflowTool.getDefaultChain(), ('my_workflow',))
-
-To set the workflow chain for the 'Document' type::
-
- workflowTool.setChainForPortalTypes(('Document',), 'my_workflow')
-
-You can pass multiple type names to set multiple chains at once. To set a
-multi-workflow chain, separate workflow names by commas. To indicate that a
-type should use the default workflow, use the special chain name '(Default)'.
-
-To get the workflow chain for the portal type 'Document'::
-
- chains = dict(workflowTool.listChainOverrides())
- defaultChain = workflowTool.getDefaultChain()
- documentChain = chains.get('Document', defaultChain)
-
- self.assertEqual(documentChain, ('my_other_workflow',))
-
-To get the current workflow chain for the content object f1::
-
- self.assertEqual(workflowTool.getChainFor(f1), ('my_workflow',))
-
-To update all permissions after changing the workflow::
-
- workflowTool.updateRoleMappings()
-
-To change the workflow state of the content object f1 by invoking the
-transaction 'publish'::
-
- workflowTool.doActionFor(f1, 'publish')
-
-Note that this performs an explicit permission check, so if the current user
-doesn't have permission to perform this workflow action, you may get an error
-indicating the action is not available. If so, use ``login()`` or
-``setRoles()`` to ensure the current user is able to change the workflow
-state.
-
-To check the current workflow state of the content object f1::
-
- self.assertEqual(workflowTool.getInfoFor(f1, 'review_state'), 'published')
-
-Properties
-----------
-
-To set the value of a property on the portal root::
-
- portal._setPropValue('title', u"My title")
-
-To assert the value of a property on the portal root::
-
- self.assertEqual(portal.getProperty('title'), u"My title")
-
-To change the value of a property in a property sheet in the
-``portal_properties`` tool::
-
- from Products.CMFCore.utils import getToolByName
-
- propertiesTool = getToolByName(portal, 'portal_properties')
- siteProperties = propertiesTool['site_properties']
-
- siteProperties._setPropValue('many_users', True)
-
-To assert the value of a property in a property sheet in the
-``portal_properties`` tool::
-
- self.assertEqual(siteProperties.getProperty('many_users'), True)
-
-Installing products and extension profiles
-------------------------------------------
-
-To apply a particular extension profile::
-
- from plone.app.testing import applyProfile
-
- applyProfile(portal, 'my.product:default')
-
-This is the preferred method of installing a product's configuration.
-
-To install an add-on product into the Plone site using the
-``portal_quickinstaller`` tool::
-
- from plone.app.testing import quickInstallProduct
-
- quickInstallProduct(portal, 'my.product')
-
-To re-install a product using the quick-installer::
-
- quickInstallProduct(portal, 'my.product', reinstall=True)
-
-Note that both of these assume the product's ZCML has been loaded, which is
-usually done during layer setup. See the layer examples above for more details
-on how to do that.
-
-When writing a product that has an installation extension profile, it is often
-desirable to write tests that inspect the state of the site after the profile
-has been applied. Some of the more common such tests are shown below.
-
-To verify that a product has been installed (e.g. as a dependency via
-``metadata.xml``)::
-
- from Products.CMFCore.utils import getToolByName
-
- quickinstaller = getToolByName(portal, 'portal_quickinstaller')
- self.assertTrue(quickinstaller.isProductInstalled('my.product'))
-
-To verify that a particular content type has been installed (e.g. via
-``types.xml``)::
-
- typesTool = getToolByName(portal, 'portal_types')
-
- self.assertNotEqual(typesTool.getTypeInfo('mytype'), None)
-
-To verify that a new catalog index has been installed (e.g. via
-``catalog.xml``)::
-
- catalog = getToolByName(portal, 'portal_catalog')
-
- self.assertTrue('myindex' in catalog.indexes())
-
-To verify that a new catalog metadata column has been added (e.g. via
-``catalog.xml``)::
-
- self.assertTrue('myattr' in catalog.schema())
-
-To verify that a new workflow has been installed (e.g. via
-``workflows.xml``)::
-
- workflowTool = getToolByName(portal, 'portal_workflow')
-
- self.assertNotEqual(workflowTool.getWorkflowById('my_workflow'), None)
-
-To verify that a new workflow has been assigned to a type (e.g. via
-``workflows.xml``)::
-
- self.assertEqual(dict(workflowTool.listChainOverrides())['mytype'], ('my_workflow',))
-
-To verify that a new workflow has been set as the default (e.g. via
-``workflows.xml``)::
-
- self.assertEqual(workflowTool.getDefaultChain(), ('my_workflow',))
-
-To test the value of a property in the ``portal_properties`` tool (e.g. set
-via ``propertiestool.xml``):::
-
- propertiesTool = getToolByName(portal, 'portal_properties')
- siteProperties = propertiesTool['site_properties']
-
- self.assertEqual(siteProperties.getProperty('some_property'), "some value")
-
-To verify that a stylesheet has been installed in the ``portal_css`` tool
-(e.g. via ``cssregistry.xml``)::
-
- cssRegistry = getToolByName(portal, 'portal_css')
-
- self.assertTrue('mystyles.css' in cssRegistry.getResourceIds())
-
-To verify that a JavaScript resource has been installed in the
-``portal_javascripts`` tool (e.g. via ``jsregistry.xml``)::
-
- jsRegistry = getToolByName(portal, 'portal_javascripts')
-
- self.assertTrue('myscript.js' in jsRegistry.getResourceIds())
-
-To verify that a new role has been added (e.g. via ``rolemap.xml``)::
-
- self.assertTrue('NewRole' in portal.valid_roles())
-
-To verify that a permission has been granted to a given set of roles (e.g. via
-``rolemap.xml``)::
-
- roles = [r['name'] for r in portal.rolesOfPermission('My Permission') if r['selected']]
- self.assertEqual(roles, ['Member', 'Manager'])
-
-Traversal
----------
-
-To traverse to a view, page template or other resource, use
-``restrictedTraverse()`` with a relative path::
-
- resource = portal.restrictedTraverse('f1/@@folder_contents')
-
-The return value is a view object, page template object, or other resource.
-It may be invoked to obtain an actual response (see below).
-
-``restrictedTraverse()`` performs an explicit security check, and so may
-raise ``Unauthorized`` if the current test user does not have permission to
-view the given resource. If you don't want that, you can use::
-
- resource = portal.unrestrictedTraverse('f1/@@folder_contents')
-
-You can call this on a folder or other content item as well, to traverse from
-that starting point, e.g. this is equivalent to the first example above::
-
- f1 = portal['f1']
- resource = f1.restrictedTraverse('@@folder_contents')
-
-Note that this traversal will not take ``IPublishTraverse`` adapters into
-account, and you cannot pass query string parameters. In fact,
-``restrictedTraverse()`` and ``unrestrictedTraverse()`` implement the type of
-traversal that happens with path expressions in TAL, which is similar, but not
-identical to URL traversal.
-
-To look up a view manually::
-
- from zope.component import getMultiAdapter
-
- view = getMultiAdapter((f1, request), name=u"folder_contents")
-
-Note that the name here should not include the ``@@`` prefix.
-
-To simulate an ``IPublishTraverse`` adapter call, presuming the view
-implements ``IPublishTraverse``::
-
- next = view.IPublishTraverse(request, u"some-name")
-
-Or, if the ``IPublishTraverse`` adapter is separate from the view::
-
- from zope.publisher.interfaces import IPublishTraverse
-
- publishTraverse = getMultiAdapter((f1, request), IPublishTraverse)
- next = view.IPublishTraverse(request, u"some-name")
-
-To simulate a form submission or query string parameters::
-
- request.form.update({
- 'name': "John Smith",
- 'age': 23
- })
-
-The ``form`` dictionary contains the marshalled request. That is, if you are
-simulating a query string parameter or posted form variable that uses a
-marshaller like ``:int`` (e.g. ``age:int`` in the example above), the value
-in the ``form`` dictionary should be marshalled (an int instead of a string,
-in the example above), and the name should be the base name (``age`` instead
-of ``age:int``).
-
-To invoke a view and obtain the response body as a string::
-
- view = f1.restrictedTraverse('@@folder_contents')
- body = view()
-
- self.assertFalse(u"An unexpected error occurred" in body)
-
-Please note that this approach is not perfect. In particular, the request
-is will not have the right URL or path information. If your view depends on
-this, you can fake it by setting the relevant keys in the request, e.g.::
-
- request.set('URL', f1.absolute_url() + '/@@folder_contents')
- request.set('ACTUAL_URL', f1.absolute_url() + '/@@folder_contents')
-
-To inspect the state of the request (e.g. after a view has been invoked)::
-
- self.assertEqual(request.get('disable_border'), True)
-
-To inspect response headers (e.g. after a view has been invoked)::
-
- response = request.response
-
- self.assertEqual(response.getHeader('content-type'), 'text/plain')
-
-Simulating browser interaction
-------------------------------
-
-End-to-end functional tests can use `zope.testbrowser`_ to simulate user
-interaction. This acts as a web browser, connecting to Zope via a special
-channel, making requests and obtaining responses.
-
- **Note:** zope.testbrowser runs entirely in Python, and does not simulate
- a JavaScript engine.
-
-Note that to use ``zope.testbrowser``, you need to use one of the functional
-testing layers, e.g. ``PLONE_FUNCTIONAL_TESTING``, or another layer
-instantiated with the ``FunctionalTesting`` class.
-
-If you want to create some initial content, you can do so either in a layer,
-or in the test itself, before invoking the test browser client. In the latter
-case, you need to commit the transaction before it becomes available, e.g.::
-
- from plone.app.testing import setRoles
- from plone.app.testing import TEST_USER_ID
-
- # Make some changes
- setRoles(portal, TEST_USER_ID, ['Manager'])
- portal.invokeFactory('Folder', 'f1', title=u"Folder 1")
- setRoles(portal, TEST_USER_ID, ['Member'])
-
- # Commit so that the test browser sees these changes
- import transaction
- transaction.commit()
-
-To obtain a new test browser client::
-
- from plone.testing.z2 import Browser
-
- # This is usually self.app (Zope root) or site.portal (test Plone site root)
- browser = Browser(app)
-
-To open a given URL::
-
- portalURL = portal.absolute_url()
- browser.open(portalURL)
-
-To inspect the response::
-
- self.assertTrue(u"Welcome" in browser.contents)
-
-To inspect response headers::
-
- self.assertEqual(browser.headers['content-type'], 'text/html; charset=utf-8')
-
-To follow a link::
-
- browser.getLink('Edit').click()
-
-This gets a link by its text. To get a link by HTML id::
-
- browser.getLink(id='edit-link').click()
-
-To verify the current URL::
-
- self.assertEqual(portalURL + '/edit', browser.url)
-
-To set a form control value::
-
- browser.getControl('Age').value = u"30"
-
-This gets the control by its associated label. To get a control by its form
-variable name::
-
- browser.getControl(name='age:int').value = u"30"
-
-See the `zope.testbrowser`_ documentation for more details on how to select
-and manipulate various types of controls.
-
-To submit a form by clicking a button::
-
- browser.getControl('Save').click()
-
-Again, this uses the label to find the control. To use the form variable
-name::
-
- browser.getControl(name='form.button.Save').click()
-
-To simulate HTTP BASIC authentication and remain logged in for all
-requests::
-
- from plone.app.testing import TEST_USER_NAME, TEST_USER_PASSWORD
-
- browser.addHeader('Authorization', 'Basic %s:%s' % (TEST_USER_NAME, TEST_USER_PASSWORD,))
-
-To simulate logging in via the login form::
-
- browser.open(portalURL + '/login_form')
- browser.getControl(name='__ac_name').value = TEST_USER_NAME
- browser.getControl(name='__ac_password').value = TEST_USER_PASSWORD
- browser.getControl(name='submit').click()
-
-To simulate logging out::
-
- browser.open(portalURL + '/logout')
-
-Debugging tips
-~~~~~~~~~~~~~~
-
-By default, only HTTP error codes (e.g. 500 Server Side Error) are shown when
-an error occurs on the server. To see more details, set ``handleErrors`` to
-False::
-
- browser.handleErrors = False
-
-To inspect the error log and obtain a full traceback of the latest entry::
-
- from Products.CMFCore.utils import getToolByName
-
- errorLog = getToolByName(portal, 'error_log')
- print errorLog.getLogEntries()[-1]['tb_text']
-
-To save the current response to an HTML file::
-
- open('/tmp/testbrowser.html', 'w').write(browser.contents)
-
-You can now open this file and use tools like Firebug to inspect the structure
-of the page. You should remove the file afterwards.
-
-Comparison with ZopeTestCase/PloneTestCase
-==========================================
-
-`plone.testing`_ and ``plone.app.testing`` have in part evolved from
-``ZopeTestCase``, which ships with Zope 2 in the ``Testing`` package, and
-`Products.PloneTestCase`_, which ships with Plone and is used by Plone itself
-as well as numerous add-on products.
-
-If you are familiar with ``ZopeTestCase`` and ``PloneTestCase``, the concepts
-of these package should be familiar to you. However, there are some important
-differences to bear in mind.
-
-* ``plone.testing`` and ``plone.app.testing`` are unburdened by the legacy
- support that ``ZopeTestCase`` and ``PloneTestCase`` have to include. This
- makes them smaller and easier to understand and maintain.
-
-* Conversely, ``plone.testing`` only works with Python 2.6 and Zope 2.12 and
- later. ``plone.app.testing`` only works with Plone 4 and later. If you need
- to write tests that run against older versions of Plone, you'll need to use
- ``PloneTestCase``.
-
-* ``ZopeTestCase``/``PloneTestCase`` were written before layers were available
- as a setup mechanism. ``plone.testing`` is very layer-oriented.
-
-* ``PloneTestCase`` provides a base class, also called ``PloneTestCase``,
- which you must use, as it performs setup and tear-down. ``plone.testing``
- moves shared state to layers and layer resources, and does not impose any
- particular base class for tests. This does sometimes mean a little more
- typing (e.g. ``self.layer['portal']`` vs. ``self.portal``), but it makes
- it much easier to control and re-use test fixtures. It also makes your
- test code simpler and more explicit.
-
-* ``ZopeTestCase`` has an ``installProduct()`` function and a corresponding
- ``installPackage()`` function. `plone.testing`_ has only an
- ``installProduct()``, which can configure any kind of Zope 2 product (i.e.
- packages in the ``Products.*`` namespace, old-style products in a special
- ``Products`` folder, or packages in any namespace that have had their ZCML
- loaded and which include a ``<five:registerPackage />`` directive in their
- configuration). Note that you must pass a full dotted name to this function,
- even for "old-style" products in the ``Products.*`` namespace, e.g.
- ``Products.LinguaPlone`` instead of ``LinguaPlone``.
-
-* On setup, ``PloneTestCase`` will load Zope 2's default ``site.zcml``. This
- in turn will load all ZCML for all packages in the ``Products.*`` namespace.
- ``plone.testing`` does not do this (and you are strongly encouraged from
- doing it yourself), because it is easy to accidentally include packages in
- your fixture that you didn't intend to be there (and which can actually
- change the fixture substantially). You should load your package's ZCML
- explicitly. See the `plone.testing`_ documentation for details.
-
-* When using ``PloneTestCase``, any package that has been loaded onto
- ``sys.path`` and which defines the ``z3c.autoinclude.plugin:plone`` entry
- point will be loaded via `z3c.autoinclude`_'s plugin mechanism. This loading
- is explicitly disabled, for the same reasons that the ``Products.*`` auto-
- loading is. You should load your packages' configuration explicitly.
-
-* ``PloneTestCase`` sets up a basic fixture that has member folder enabled,
- and in which the test user's member folder is available as ``self.folder``.
- The ``plone_workflow`` workflow is also installed as the default.
- ``plone.app.testing`` takes a more minimalist approach. To create a test
- folder owned by the test user that is similar to ``self.folder`` in a
- ``PloneTestCase``, you can do::
-
- import unittest2 as unittest
- from plone.app.testing import TEST_USER_ID, setRoles
- from plone.app.testing import PLONE_INTEGRATION_TESTING
-
- class MyTest(unitest.TestCase):
-
- layer = PLONE_INTEGRATION_TESTING
-
- def setUp(self):
- self.portal = self.layer['portal']
-
- setRoles(self.portal, TEST_USER_ID, ['Manager'])
- self.portal.invokeFactory('Folder', 'test-folder')
- setRoles(self.portal, TEST_USER_ID, ['Member'])
-
- self.folder = self.portal['test-folder']
-
- You could of course do this type of setup in your own layer and expose it
- as a resource instead.
-
-* To use `zope.testbrowser`_ with ``PloneTestCase``, you should use its
- ``FunctionalTestCase`` as a base class, and then use the following pattern::
-
- from Products.Five.testbrowser import Browser
- browser = Browser()
-
- The equivalent pattern in ``plone.app.testing`` is to use the
- ``FunctionalTesting`` test lifecycle layer (see example above), and then
- use::
-
- from plone.testing.z2 import Browser
- browser = Browser(self.layer['app'])
-
- Also note that if you have made changes to the fixture prior to calling
- ``browser.open()``, they will *not* be visible until you perform an
- explicit commit. See the ``zope.testbrowser`` examples above for details.
-
-.. _plone.testing: http://pypi.python.org/pypi/plone.testing
-.. _zope.testing: http://pypi.python.org/pypi/zope.testing
-.. _z3c.autoinclude: http://pypi.python.org/pypi/z3c.autoinclude
-.. _zope.testbrowser: http://pypi.python.org/pypi/zope.testbrowser
-.. _Products.PloneTestCase: http://pypi.python.org/pypi/Products.PloneTestCase
diff --git a/README.rst b/README.rst
new file mode 120000
index 0000000..dd7a278
--- /dev/null
+++ b/README.rst
@@ -0,0 +1 @@
+docs/source/README.rst
\ No newline at end of file
diff --git a/docs/source/README.rst b/docs/source/README.rst
new file mode 100644
index 0000000..94d8b58
--- /dev/null
+++ b/docs/source/README.rst
@@ -0,0 +1,1491 @@
+Introduction
+============
+
+.. contents:: Table of contents
+
+``plone.app.testing`` provides tools for writing integration and functional
+tests for code that runs on top of Plone. It is based on `plone.testing`_.
+If you are unfamiliar with ``plone.testing``, the concept of layers, or the
+`zope.testing`_ testrunner, please take a look at the the ``plone.testing``
+documentation. In fact, even if you are working exclusively with Plone, you
+are likely to want to use some of its features for unit testing.
+
+In short, ``plone.app.testing`` includes:
+
+* A set of layers that set up fixtures containing a Plone site, intended for
+ writing integration and functional tests.
+* A collection of helper functions, some useful for writing your own layers
+ and some applicable to tests themselves.
+* A convenient layer base class, extending ``plone.testing.Layer``, which
+ makes it easier to write custom layers extending the Plone site fixture,
+ with proper isolation and tear-down.
+* Cleanup hooks for ``zope.testing.cleanup`` to clean up global state found
+ in a Plone installation. This is useful for unit testing.
+
+Compatibility
+-------------
+
+``plone.app.testing`` 4.x works with Plone 4 and Zope 2.12. It may work with
+newer versions. It will not work with earlier versions. Use
+``plone.app.testing`` 3.x for Plone 3 and Zope 2.10.
+
+Installation and usage
+======================
+
+To use ``plone.app.testing`` in your own package, you need to add it as a
+dependency. Most people prefer to keep test-only dependencies separate, so
+that they do not need to be installed in scenarios (such as on a production
+server) where the tests will not be run. This can be achieved using a
+``test`` extra.
+
+In ``setup.py``, add or modify the ``extras_require`` option, like so::
+
+ extras_require = {
+ 'test': [
+ 'plone.app.testing',
+ ]
+ },
+
+This will also include ``plone.testing``, with the ``[z2]``, ``[zca]`` and
+``[zodb]`` extras (which ``plone.app.testing`` itself relies on).
+
+Please see the `plone.testing`_ documentation for more details about how to
+add a test runner to your buildout, and how to write and run tests.
+
+Layer reference
+===============
+
+This package contains a layer class,
+``plone.app.testing.layers.PloneFixture``, which sets up a Plone site fixture.
+It is combined with other layers from `plone.testing`_ to provide a number of
+layer instances. It is important to realise that these layers all have the
+same fundamental fixture: they just manage test setup and tear-down
+differently.
+
+When set up, the fixture will:
+
+* Create a ZODB sandbox, via a stacked ``DemoStorage``. This ensures
+ persistent changes made during layer setup can be cleanly torn down.
+* Configure a global component registry sandbox. This ensures that global
+ component registrations (e.g. as a result of loading ZCML configuration)
+ can be cleanly torn down.
+* Create a configuration context with the ``disable-autoinclude`` feature
+ set. This has the effect of stopping Plone from automatically loading the
+ configuration of any installed package that uses the
+ ``z3c.autoinclude.plugin:plone`` entry point via `z3c.autoinclude`_. (This
+ is to avoid accidentally polluting the test fixture - custom layers should
+ load packages' ZCML configuration explicitly if required).
+* Install a number of Zope 2-style products on which Plone depends.
+* Load the ZCML for these products, and for ``Products.CMFPlone``, which in
+ turn pulls in the configuration for the core of Plone.
+* Create a default Plone site, with the default theme enabled, but with no
+ default content.
+* Add a user to the root user folder with the ``Manager`` role.
+* Add a test user to this instance with the ``Member`` role.
+
+For each test:
+
+* The test user is logged in
+* The local component site is set
+* Various global caches are cleaned up
+
+Various constants in the module ``plone.app.testing.interfaces`` are defined
+to describe this environment:
+
++----------------------+--------------------------------------------------+
+| **Constant** | **Purpose** |
++----------------------+--------------------------------------------------+
+| PLONE_SITE_ID | The id of the Plone site object inside the Zope |
+| | application root. |
++----------------------+--------------------------------------------------+
+| PLONE_SITE_TITLE | The title of the Plone site |
++----------------------+--------------------------------------------------+
+| DEFAULT_LANGUAGE | The default language of the Plone site ('en') |
++----------------------+--------------------------------------------------+
+| TEST_USER_ID | The id of the test user |
++----------------------+--------------------------------------------------+
+| TEST_USER_NAME | The username of the test user |
++----------------------+--------------------------------------------------+
+| TEST_USER_PASSWORD | The password of the test user |
++----------------------+--------------------------------------------------+
+| TEST_USER_ROLES | The default global roles of the test user - |
+| | ('Member',) |
++----------------------+--------------------------------------------------+
+| SITE_OWNER_NAME | The username of the user owning the Plone site. |
++----------------------+--------------------------------------------------+
+| SITE_OWNER_PASSWORD | The password of the user owning the Plone site. |
++----------------------+--------------------------------------------------+
+
+All the layers also expose a resource in addition to those from their
+base layers, made available during tests:
+
+``portal``
+ The Plone site root.
+
+Plone site fixture
+------------------
+
++------------+--------------------------------------------------+
+| Layer: | ``plone.app.testing.PLONE_FIXTURE`` |
++------------+--------------------------------------------------+
+| Class: | ``plone.app.testing.layers.PloneFixture`` |
++------------+--------------------------------------------------+
+| Bases: | ``plone.testing.z2.STARTUP`` |
++------------+--------------------------------------------------+
+| Resources: | |
++------------+--------------------------------------------------+
+
+This layer sets up the Plone site fixture on top of the ``z2.STARTUP``
+fixture.
+
+You should not use this layer directly, as it does not provide any test
+lifecycle or transaction management. Instead, you should use a layer
+created with either the ``IntegrationTesting`` or ``FunctionalTesting``
+classes, as outlined below.
+
+Integration and functional testing test lifecycles
+--------------------------------------------------
+
+``plone.app.testing`` comes with two layer classes, ``IntegrationTesting``
+and ``FunctionalTesting``, which derive from the corresponding layer classes
+in ``plone.testing.z2``.
+
+These classes set up the ``app``, ``request`` and ``portal`` resources, and
+reset the fixture (including various global caches) between each test run.
+
+As with the classes in ``plone.testing``, the ``IntegrationTesting`` class
+will create a new transaction for each test and roll it back on test tear-
+down, which is efficient for integration testing, whilst ``FunctionalTesting``
+will create a stacked ``DemoStorage`` for each test and pop it on test tear-
+down, making it possible to exercise code that performs an explicit commit
+(e.g. via tests that use ``zope.testbrowser``).
+
+When creating a custom fixture, the usual pattern is to create a new layer
+class that has ``PLONE_FIXTURE`` as its default base, instantiating that as a
+separate "fixture" layer. This layer is not to be used in tests directly,
+since it won't have test/transaction lifecycle management, but represents a
+shared fixture, potentially for both functional and integration testing. It
+is also the point of extension for other layers that follow the same pattern.
+
+Once this fixture has been defined, "end-user" layers can be defined using
+the ``IntegrationTesting`` and ``FunctionalTesting`` classes. For example::
+
+ from plone.testing import Layer
+ from plone.app.testing import PLONE_FIXTURE
+ from plone.app.testing import IntegrationTesting, FunctionalTesting
+
+ class MyFixture(Layer):
+ defaultBases = (PLONE_FIXTURE,)
+
+ ...
+
+ MY_FIXTURE = MyFixture()
+
+ MY_INTEGRATION_TESTING = IntegrationTesting(bases=(MY_FIXTURE,), name="MyFixture:Integration")
+ MY_FUNCTIONAL_TESTING = FunctionalTesting(bases=(MY_FIXTURE,), name="MyFixture:Functional")
+
+See the ``PloneSandboxLayer`` layer below for a more comprehensive example.
+
+Plone integration testing
+-------------------------
+
++------------+--------------------------------------------------+
+| Layer: | ``plone.app.testing.PLONE_INTEGRATION_TESTING`` |
++------------+--------------------------------------------------+
+| Class: | ``plone.app.testing.layers.IntegrationTesting`` |
++------------+--------------------------------------------------+
+| Bases: | ``plone.app.testing.PLONE_FIXTURE`` |
++------------+--------------------------------------------------+
+| Resources: | ``portal`` (test setup only) |
++------------+--------------------------------------------------+
+
+This layer can be used for integration testing against the basic
+``PLONE_FIXTURE`` layer.
+
+You can use this directly in your tests if you do not need to set up any
+other shared fixture.
+
+However, you would normally not extend this layer - see above.
+
+
+Plone functional testing
+------------------------
+
++------------+--------------------------------------------------+
+| Layer: | ``plone.app.testing.PLONE_FUNCTIONAL_TESTING`` |
++------------+--------------------------------------------------+
+| Class: | ``plone.app.testing.layers.FunctionalTesting`` |
++------------+--------------------------------------------------+
+| Bases: | ``plone.app.testing.PLONE_FIXTURE`` |
++------------+--------------------------------------------------+
+| Resources: | ``portal`` (test setup only) |
++------------+--------------------------------------------------+
+
+This layer can be used for functional testing against the basic
+``PLONE_FIXTURE`` layer, for example using ``zope.testbrowser``.
+
+You can use this directly in your tests if you do not need to set up any
+other shared fixture.
+
+Again, you would normally not extend this layer - see above.
+
+Plone ZServer
+-------------
+
++------------+--------------------------------------------------+
+| Layer: | ``plone.app.testing.PLONE_ZSERVER`` |
++------------+--------------------------------------------------+
+| Class: | ``plone.testing.z2.ZServer`` |
++------------+--------------------------------------------------+
+| Bases: | ``plone.app.testing.PLONE_FUNCTIONAL_TESTING`` |
++------------+--------------------------------------------------+
+| Resources: | ``portal`` (test setup only) |
++------------+--------------------------------------------------+
+
+This is layer is intended for functional testing using a live, running HTTP
+server, e.g. using Selenium or Windmill.
+
+Again, you would not normally extend this layer. To create a custom layer
+that has a running ZServer, you can use the same pattern as this one, e.g.::
+
+ from plone.testing import Layer
+ from plone.testing import z2
+ from plone.app.testing import PLONE_FIXTURE
+ from plone.app.testing import FunctionalTesting
+
+ class MyFixture(Layer):
+ defaultBases = (PLONE_FIXTURE,)
+
+ ...
+
+ MY_FIXTURE = MyFixture()
+ MY_ZSERVER = FunctionalTesting(bases=(MY_FIXTURE, z2.ZSERVER_FIXTURE), name='MyFixture:ZServer')
+
+See the description of the ``z2.ZSERVER`` layer in `plone.testing`_
+for further details.
+
+Plone FTP server
+----------------
+
++------------+--------------------------------------------------+
+| Layer: | ``plone.app.testing.PLONE_FTP_SERVER`` |
++------------+--------------------------------------------------+
+| Class: | ``plone.app.testing.layers.FunctionalTesting`` |
++------------+--------------------------------------------------+
+| Bases: | ``plone.app.testing.PLONE_FIXTURE`` |
+| | ``plone.testing.z2.ZSERVER_FIXTURE`` |
++------------+--------------------------------------------------+
+| Resources: | ``portal`` (test setup only) |
++------------+--------------------------------------------------+
+
+This is layer is intended for functional testing using a live FTP server.
+
+It is semantically equivalent to the ``PLONE_ZSERVER`` layer.
+
+See the description of the ``z2.FTP_SERVER`` layer in `plone.testing`_
+for further details.
+
+Helper functions
+================
+
+A number of helper functions are provided for use in tests and custom layers.
+
+Plone site context manager
+--------------------------
+
+``ploneSite(db=None, connection=None, environ=None)``
+ Use this context manager to access and make changes to the Plone site
+ during layer setup. In most cases, you will use it without arguments,
+ but if you have special needs, you can tie it to a particular database
+ instance. See the description of the ``zopeApp()`` context manager in
+ `plone.testing`_ (which this context manager uses internally) for details.
+
+ The usual pattern is to call it during ``setUp()`` or ``tearDown()`` in
+ your own layers::
+
+ from plone.testing import Layer
+ from plone.app.testing import ploneSite
+
+ class MyLayer(Layer):
+
+ def setUp(self):
+
+ ...
+
+ with ploneSite() as portal:
+
+ # perform operations on the portal, e.g.
+ portal.title = u"New title"
+
+ Here, ``portal`` is the Plone site root. A transaction is begun before
+ entering the ``with`` block, and will be committed upon exiting the block,
+ unless an exception is raised, in which case it will be rolled back.
+
+ Inside the block, the local component site is set to the Plone site root,
+ so that local component lookups should work.
+
+ **Warning:** Do not attempt to load ZCML files inside a ``ploneSite``
+ block. Because the local site is set to the Plone site, you may end up
+ accidentally registering components in the local site manager, which can
+ cause pickling errors later.
+
+ **Note:** You should not use this in a test, or in a ``testSetUp()`` or
+ ``testTearDown()`` method of a layer based on one of the layer in this
+ package. Use the ``portal`` resource instead.
+
+ **Also note:** If you are writing a layer setting up a Plone site fixture,
+ you may want to use the ``PloneSandboxLayer`` layer base class, and
+ implement the ``setUpZope()``, ``setUpPloneSite()``, ``tearDownZope()``
+ and/or ``tearDownPloneSite()`` methods instead. See below.
+
+User management
+---------------
+
+``login(portal, userName)``
+ Simulate login as the given user. This is based on the ``z2.login()``
+ helper in `plone.testing`_, but instead of passing a specific user folder,
+ you pass the portal (e.g. as obtained via the ``portal`` layer resource).
+
+ For example::
+
+ import unittest2 as unittest
+
+ from plone.app.testing import PLONE_INTEGRATION_TESTING
+ from plone.app.testing import TEST_USER_NAME
+ from plone.app.testing import login
+
+ ...
+
+ class MyTest(unittest.TestCase):
+
+ layer = PLONE_INTEGRATION_TESTING
+
+ def test_something(self):
+ portal = self.layer['portal']
+ login(portal, TEST_USER_NAME)
+
+ ...
+
+``logout()``
+ Simulate logging out, i.e. becoming the anonymous user. This is equivalent
+ to the ``z2.logout()`` helper in `plone.testing`_.
+
+ For example::
+
+ import unittest2 as unittest
+
+ from plone.app.testing import PLONE_INTEGRATION_TESTING
+ from plone.app.testing import logout
+
+ ...
+
+ class MyTest(unittest.TestCase):
+
+ layer = PLONE_INTEGRATION_TESTING
+
+ def test_something(self):
+ portal = self.layer['portal']
+ logout()
+
+ ...
+
+``setRoles(portal, userId, roles)``
+ Set the roles for the given user. ``roles`` is a list of roles.
+
+ For example::
+
+ import unittest2 as unittest
+
+ from plone.app.testing import PLONE_INTEGRATION_TESTING
+ from plone.app.testing import TEST_USER_ID
+ from plone.app.testing import setRoles
+
+ ...
+
+ class MyTest(unittest.TestCase):
+
+ layer = PLONE_INTEGRATION_TESTING
+
+ def test_something(self):
+ portal = self.layer['portal']
+ setRoles(portal, TEST_USER_ID, ['Manager'])
+
+Product and profile installation
+--------------------------------
+
+``applyProfile(portal, profileName)``
+ Install a GenericSetup profile (usually an extension profile) by name,
+ using the ``portal_setup`` tool. The name is normally made up of a package
+ name and a profile name. Do not use the ``profile-`` prefix.
+
+ For example::
+
+ from plone.testing import Layer
+
+ from plone.app.testing import ploneSite
+ from plone.app.testing import applyProfile
+
+ ...
+
+ class MyLayer(Layer):
+
+ ...
+
+ def setUp(self):
+
+ ...
+
+ with ploneSite() as portal:
+ applyProfile(portal, 'my.product:default')
+
+ ...
+
+``quickInstallProduct(portal, productName, reinstall=False)``
+ Use this function to install a particular product into the given Plone
+ site, using the ``portal_quickinstaller`` tool. If ``reinstall`` is
+ ``False`` and the product is already installed, nothing will happen; if
+ ``reinstall`` is ``True``, the product will be reinstalled. The
+ ``productName`` should be a full dotted name, e.g. ``Products.MyProduct``,
+ or ``my.product``.
+
+ For example::
+
+ from plone.testing import Layer
+
+ from plone.app.testing import ploneSite
+ from plone.app.testing import quickInstallProduct
+
+ ...
+
+ class MyLayer(Layer):
+
+ ...
+
+ def setUp(self):
+
+ ...
+
+ with ploneSite() as portal:
+ quickInstallProduct(portal, 'my.product')
+
+ ...
+
+Component architecture sandboxing
+---------------------------------
+
+``pushGlobalRegistry(portal, new=None, name=None)``
+ Create or obtain a stack of global component registries, and push a new
+ registry to the top of the stack. This allows Zope Component Architecture
+ registrations (e.g. loaded via ZCML) to be effectively torn down.
+
+ If you are going to use this function, please read the corresponding
+ documentation for ``zca.pushGlobalRegistry()`` in `plone.testing`_. In
+ particular, note that you *must* reciprocally call ``popGlobalRegistry()``
+ (see below).
+
+ This helper is based on ``zca.pushGlobalRegistry()``, but will also fix
+ up the local component registry in the Plone site ``portal`` so that it
+ has the correct bases.
+
+ For example::
+
+ from plone.testing import Layer
+
+ from plone.app.testing import ploneSite
+ from plone.app.testing import pushGlobalRegistry
+ from plone.app.testing import popGlobalRegistry
+
+ ...
+
+ class MyLayer(Layer):
+
+ ...
+
+ def setUp(self):
+
+ ...
+
+ with ploneSite() as portal:
+ pushGlobalRegistry(portal)
+
+ ...
+
+``popGlobalRegistry(portal)``
+ Tear down the top of the component architecture stack, as created with
+ ``pushGlobalRegistry()``
+
+ For example::
+
+ ...
+
+ def tearDown(self):
+
+ with ploneSite() as portal:
+ popGlobalRegistry(portal)
+
+Global state cleanup
+--------------------
+
+``tearDownMultiPluginRegistration(pluginName)``
+ PluggableAuthService "MultiPlugins" are kept in a global registry. If
+ you have registered a plugin, e.g. using the ``registerMultiPlugin()``
+ API, you should tear that registration down in your layer's ``tearDown()``
+ method. You can use this helper, passing a plugin name.
+
+ For example::
+
+ from plone.testing import Layer
+
+ from plone.app.testing import ploneSite
+ from plone.app.testing import tearDownMultiPluginRegistration
+
+ ...
+
+ class MyLayer(Layer):
+
+ ...
+
+ def tearDown(self):
+
+ tearDownMultiPluginRegistration('MyPlugin')
+
+ ...
+
+Layer base class
+================
+
+If you are writing a custom layer to test your own Plone add-on product, you
+will often want to do the following on setup:
+
+1. Stack a new ``DemoStorage`` on top of the one from the base layer. This
+ ensures that any persistent changes performed during layer setup can be
+ torn down completely, simply by popping the demo storage.
+
+2. Stack a new ZCML configuration context. This keeps separate the information
+ about which ZCML files were loaded, in case other, independent layers want
+ to load those same files after this layer has been torn down.
+
+3. Push a new global component registry. This allows you to register
+ components (e.g. by loading ZCML or using the test API from
+ ``zope.component``) and tear down those registration easily by popping the
+ component registry.
+
+4. Load your product's ZCML configuration
+
+5. Install the product into the test fixture Plone site
+
+Of course, you may wish to make other changes too, such as creating some base
+content or changing some settings.
+
+On tear-down, you will then want to:
+
+1. Remove any Pluggable Authentication Service "multi-plugins" that were added
+ to the global registry during setup.
+
+2. Pop the global component registry to unregister components loaded via ZCML.
+
+3. Pop the configuration context resource to restore its state.
+
+4. Pop the ``DemoStorage`` to undo any persistent changes.
+
+If you have made other changes on setup that are not covered by this broad
+tear-down, you'll also want to tear those down explicitly here.
+
+Stacking a demo storage and component registry is the safest way to avoid
+fixtures bleeding between tests. However, it can be tricky to ensure that
+everything happens in the right order.
+
+To make things easier, you can use the ``PloneSandboxLayer`` layer base class.
+This extends ``plone.testing.Layer`` and implements ``setUp()`` and
+``tearDown()`` for you. You simply have to override one or more of the
+following methods:
+
+``setUpZope(self, app, configurationContext)``
+ This is called during setup. ``app`` is the Zope application root.
+ ``configurationContext`` is a newly stacked ZCML configuration context.
+ Use this to load ZCML, install products using the helper
+ ``plone.testing.z2.installProduct()``, or manipulate other global state.
+
+``setUpPloneSite(self, portal)``
+ This is called during setup. ``portal`` is the Plone site root as
+ configured by the ``ploneSite()`` context manager. Use this to make
+ persistent changes inside the Plone site, such as installing products
+ using the ``applyProfile()`` or ``quickInstallProduct()`` helpers, or
+ setting up default content.
+
+``tearDownZope(self, app)``
+ This is called during tear-down, before the global component registry and
+ stacked ``DemoStorage`` are popped. Use this to tear down any additional
+ global state.
+
+ **Note:** Global component registrations PAS multi-plugin registrations are
+ automatically torn down. Product installations are not, so you should use
+ the ``uninstallProduct()`` helper if any products were installed during
+ ``setUpZope()``.
+
+``tearDownPloneSite(self, portal)``
+ This is called during tear-down, before the global component registry and
+ stacked ``DemoStorage`` are popped. During this method, the local
+ component site hook is set, giving you access to local components.
+
+ **Note:** Persistent changes to the ZODB are automatically torn down by
+ virtue of a stacked ``DemoStorage``. Thus, this method is less commonly
+ used than the others described here.
+
+Let's show a more comprehensive example of what such a layer may look like.
+Imagine we have a product ``my.product``. It has a ``configure.zcml`` file
+that loads some components and registers a ``GenericSetup`` profile, making it
+installable in the Plone site. On layer setup, we want to load the product's
+configuration and install it into the Plone site.
+
+The layer would conventionally live in a module ``testing.py`` at the root of
+the package, i.e. ``my.product.testing``::
+
+ from plone.app.testing import PloneSandboxLayer
+ from plone.app.testing import PLONE_FIXTURE
+ from plone.app.testing import IntegrationTesting
+
+ from plone.testing import z2
+
+ class MyProduct(PloneSandboxLayer):
+
+ defaultBases = (PLONE_FIXTURE,)
+
+ def setUpZope(self, app, configurationContext):
+ # Load ZCML
+ import my.product
+ self.loadZCML(package=my.product)
+
+ # Install product and call its initialize() function
+ z2.installProduct(app, 'my.product')
+
+ # Note: you can skip this if my.product is not a Zope 2-style
+ # product, i.e. it is not in the Products.* namespace and it
+ # does not have a <five:registerPackage /> directive in its
+ # configure.zcml.
+
+ def setUpPloneSite(self, portal):
+ # Install into Plone site using portal_setup
+ self.applyProfile(portal, 'my.product:default')
+
+ def tearDownZope(self, app):
+ # Uninstall product
+ z2.uninstallProduct(app, 'my.product')
+
+ # Note: Again, you can skip this if my.product is not a Zope 2-
+ # style product
+
+ MY_PRODUCT_FIXTURE = MyProduct()
+ MY_PRODUCT_INTEGRATION_TESTING = IntegrationTesting(bases=(MY_PRODUCT_FIXTURE,), name="MyProduct:Integration")
+
+Here, ``MY_PRODUCT_FIXTURE`` is the "fixture" base layer. Other layers can
+use this as a base if they want to build on this fixture, but it would not
+be used in tests directly. For that, we have crated an ``IntegrationTesting``
+instance, ``MY_PRODUCT_INTEGRATION_TESTING``.
+
+Of course, we could have created a ``FunctionalTesting`` instance as
+well, e.g.::
+
+ MY_PRODUCT_FUNCTIONAL_TESTING = FunctionalTesting(bases=(MY_PRODUCT_FIXTURE,), name="MyProduct:Functional")
+
+Of course, we could do a lot more in the layer setup. For example, let's say
+the product had a content type 'my.product.page' and we wanted to create some
+test content. We could do that with::
+
+ from plone.app.testing import TEST_USER_ID
+ from plone.app.testing import TEST_USER_NAME
+ from plone.app.testing import login
+ from plone.app.testing import setRoles
+
+ ...
+
+ def setUpPloneSite(self, portal):
+
+ ...
+
+ setRoles(portal, TEST_USER_ID, ['Manager'])
+ login(portal, TEST_USER_NAME)
+ portal.invokeFactory('my.product.page', 'page-1', title=u"Page 1")
+ setRoles(portal, TEST_USER_ID, ['Member'])
+
+ ...
+
+Note that unlike in a test, there is no user logged in at layer setup time,
+so we have to explicitly log in as the test user. Here, we also grant the test
+user the ``Manager`` role temporarily, to allow object construction (which
+performs an explicit permission check).
+
+ **Note:** Automatic tear down suffices for all the test setup above. If
+ the only changes made during layer setup are to persistent, in-ZODB data,
+ or the global component registry then no additional tear-down is required.
+ For any other global state being managed, you should write a
+ ``tearDownPloneSite()`` method to perform the necessary cleanup.
+
+Given this layer, we could write a test (e.g. in ``tests.py``) like::
+
+ import unittest2 as unittest
+ from my.product.testing import MY_PRODUCT_INTEGRATION_TESTING
+
+ class IntegrationTest(unittest.TestCase):
+
+ layer = MY_PRODUCT_INTEGRATION_TESTING
+
+ def test_page_dublin_core_title(self):
+ portal = self.layer['portal']
+
+ page1 = portal['page-1']
+ page1.title = u"Some title"
+
+ self.assertEqual(page1.Title(), u"Some title")
+
+Please see `plone.testing`_ for more information about how to write and run
+tests and assertions.
+
+Common test patterns
+====================
+
+`plone.testing`_'s documentation contains details about the fundamental
+techniques for writing tests of various kinds. In a Plone context, however,
+some patterns tend to crop up time and again. Below, we will attempt to
+catalogue some of the more commonly used patterns via short code samples.
+
+The examples in this section are all intended to be used in tests. Some may
+also be useful in layer set-up/tear-down. We have used ``unittest`` syntax
+here, although most of these examples could equally be adopted to doctests.
+
+We will assume that you are using a layer that has ``PLONE_FIXTURE`` as a base
+(whether directly or indirectly) and uses the ``IntegrationTesting`` or
+``FunctionalTesting`` classes as shown above.
+
+We will also assume that the variables ``app``, ``portal`` and ``request`` are
+defined from the relative layer resources, e.g. with::
+
+ app = self.layer['app']
+ portal = self.layer['portal']
+ request = self.layer['request']
+
+Note that in a doctest set up using the ``layered()`` function from
+``plone.testing``, ``layer`` is in the global namespace, so you would do e.g.
+``portal = layer['portal']``.
+
+Where imports are required, they are shown alongside the code example. If
+a given import or variable is used more than once in the same section, it
+will only be shown once.
+
+Basic content management
+------------------------
+
+To create a content item of type 'Folder' with the id 'f1' in the root of
+the portal::
+
+ portal.invokeFactory('Folder', 'f1', title=u"Folder 1")
+
+The ``title`` argument is optional. Other basic properties, like
+``description``, can be set as well.
+
+Note that this may fail with an ``Unauthorized`` exception, since the test
+user won't normally have permissions to add content in the portal root, and
+the ``invokeFactory()`` method performs an explicit security check. You can
+set the roles of the test user to ensure that he has the necessary
+permissions::
+
+ from plone.app.testing import setRoles
+ from plone.app.testing import TEST_USER_ID
+
+ setRoles(portal, TEST_USER_ID, ['Manager'])
+ portal.invokeFactory('Folder', 'f1', title=u"Folder 1")
+
+To obtain this object, acquisition-wrapped in its parent::
+
+ f1 = portal['f1']
+
+To make an assertion against an attribute or method of this object::
+
+ self.assertEqual(f1.Title(), u"Folder 1")
+
+To modify the object::
+
+ f1.setTitle(u"Some title")
+
+To add another item inside the folder f1::
+
+ f1.invokeFactory('Document', 'd1', title=u"Document 1")
+ d1 = f1['d1']
+
+To check if an object is in a container::
+
+ self.assertTrue('f1' in portal)
+
+To delete an object from a container:
+
+ del portal['f1']
+
+There is no content or workflows installed by default. You can enable workflows::
+
+ portal.portal_workflow.setDefaultChain("simple_publication_workflow")
+
+Searching
+---------
+
+To obtain the ``portal_catalog`` tool::
+
+ from Products.CMFCore.utils import getToolByName
+
+ catalog = getToolByName(portal, 'portal_catalog')
+
+To search the catalog::
+
+ results = catalog(portal_type="Document")
+
+Keyword arguments are search parameters. The result is a lazy list. You can
+call ``len()`` on it to get the number of search results, or iterate through
+it. The items in the list are catalog brains. They have attributes that
+correspond to the "metadata" columns configured for the catalog, e.g.
+``Title``, ``Description``, etc. Note that these are simple attributes (not
+methods), and contain the value of the corresponding attribute or method from
+the source object at the time the object was cataloged (i.e. they are not
+necessarily up to date).
+
+To make assertions against the search results::
+
+ self.assertEqual(len(results), 1)
+
+ # Copy the list into memory so that we can use [] notation
+ results = list(results)
+
+ # Check the first (and in this case only) result in the list
+ self.assertEqual(results[0].Title, u"Document 1")
+
+To get the path of a given item in the search results::
+
+ self.assertEqual(resuls[0].getPath(), portal.absolute_url_path() + '/f1/d1')
+
+To get an absolute URL::
+
+ self.assertEqual(resuls[0].getURL(), portal.absolute_url() + '/f1/d1')
+
+To get the original object::
+
+ obj = results[0].getObject()
+
+To re-index an object d1 so that its catalog information is up to date::
+
+ d1.reindexObject()
+
+User management
+---------------
+
+To create a new user::
+
+ from Products.CMFCore.utils import getToolByName
+
+ acl_users = getToolByName(portal, 'acl_users')
+
+ acl_users.userFolderAddUser('user1', 'secret', ['Member'], [])
+
+The arguments are the username (which will also be the user id), the password,
+a list of roles, and a list of domains (rarely used).
+
+To make a particular user active ("logged in") in the integration testing
+environment use the ``login`` method and pass it the username::
+
+ from plone.app.testing import login
+
+ login(portal, 'user1')
+
+To log out (become anonymous)::
+
+ from plone.app.testing import logout
+
+ logout()
+
+To obtain the current user::
+
+ from AccessControl import getSecurityManager
+
+ user = getSecurityManager().getUser()
+
+To obtain a user by name::
+
+ user = acl_users.getUser('user1')
+
+Or by user id (id and username are often the same, but can differ in real-world
+scenarios)::
+
+ user = acl_users.getUserById('user1')
+
+To get the user's user name::
+
+ userName = user.getUserName()
+
+To get the user's id::
+
+ userId = user.getId()
+
+Permissions and roles
+---------------------
+
+To get a user's roles in a particular context (taking local roles into
+account)::
+
+ from AccessControl import getSecurityManager
+
+ user = getSecurityManager().getUser()
+
+ self.assertEqual(user.getRolesInContext(portal), ['Member'])
+
+To change the test user's roles::
+
+ from plone.app.testing import setRoles
+ from plone.app.testing import TEST_USER_ID
+
+ setRoles(portal, TEST_USER_ID, ['Member', 'Manager'])
+
+Pass a different user name to change the roles of another user.
+
+To grant local roles to a user in the folder f1::
+
+ f1.manage_setLocalRoles(TEST_USER_ID, ('Reviewer',))
+
+To check the local roles of a given user in the folder 'f1'::
+
+ self.assertEqual(f1.get_local_roles_for_userid(TEST_USER_ID), ('Reviewer',))
+
+To grant the 'View' permission to the roles 'Member' and 'Manager' in the
+portal root without acquiring additional roles from its parents::
+
+ portal.manage_permission('View', ['Member', 'Manager'], acquire=False)
+
+This method can also be invoked on a folder or individual content item.
+
+To assert which roles have the permission 'View' in the context of the
+portal::
+
+ roles = [r['name'] for r in portal.rolesOfPermission('View') if r['selected']]
+ self.assertEqual(roles, ['Member', 'Manager'])
+
+To assert which permissions have been granted to the 'Reviewer' role in the
+context of the portal::
+
+ permissions = [p['name'] for p in portal.permissionsOfRole('Reviewer') if p['selected']]
+ self.assertTrue('Review portal content' in permissions)
+
+To add a new role::
+
+ portal._addRole('Tester')
+
+This can now be assigned to users globally (using the ``setRoles`` helper)
+or locally (using ``manage_setLocalRoles()``).
+
+To assert which roles are available in a given context::
+
+ self.assertTrue('Tester' in portal.valid_roles())
+
+Workflow
+--------
+
+To set the default workflow chain::
+
+ from Products.CMFCore.utils import getToolByName
+
+ workflowTool = getToolByName(portal, 'portal_workflow')
+
+ workflowTool.setDefaultChain('my_workflow')
+
+In Plone, most chains contain only one workflow, but the ``portal_workflow``
+tool supports longer chains, where an item is subject to more than one
+workflow simultaneously.
+
+To set a multi-workflow chain, separate workflow names by commas.
+
+To get the default workflow chain::
+
+ self.assertEqual(workflowTool.getDefaultChain(), ('my_workflow',))
+
+To set the workflow chain for the 'Document' type::
+
+ workflowTool.setChainForPortalTypes(('Document',), 'my_workflow')
+
+You can pass multiple type names to set multiple chains at once. To set a
+multi-workflow chain, separate workflow names by commas. To indicate that a
+type should use the default workflow, use the special chain name '(Default)'.
+
+To get the workflow chain for the portal type 'Document'::
+
+ chains = dict(workflowTool.listChainOverrides())
+ defaultChain = workflowTool.getDefaultChain()
+ documentChain = chains.get('Document', defaultChain)
+
+ self.assertEqual(documentChain, ('my_other_workflow',))
+
+To get the current workflow chain for the content object f1::
+
+ self.assertEqual(workflowTool.getChainFor(f1), ('my_workflow',))
+
+To update all permissions after changing the workflow::
+
+ workflowTool.updateRoleMappings()
+
+To change the workflow state of the content object f1 by invoking the
+transaction 'publish'::
+
+ workflowTool.doActionFor(f1, 'publish')
+
+Note that this performs an explicit permission check, so if the current user
+doesn't have permission to perform this workflow action, you may get an error
+indicating the action is not available. If so, use ``login()`` or
+``setRoles()`` to ensure the current user is able to change the workflow
+state.
+
+To check the current workflow state of the content object f1::
+
+ self.assertEqual(workflowTool.getInfoFor(f1, 'review_state'), 'published')
+
+Properties
+----------
+
+To set the value of a property on the portal root::
+
+ portal._setPropValue('title', u"My title")
+
+To assert the value of a property on the portal root::
+
+ self.assertEqual(portal.getProperty('title'), u"My title")
+
+To change the value of a property in a property sheet in the
+``portal_properties`` tool::
+
+ from Products.CMFCore.utils import getToolByName
+
+ propertiesTool = getToolByName(portal, 'portal_properties')
+ siteProperties = propertiesTool['site_properties']
+
+ siteProperties._setPropValue('many_users', True)
+
+To assert the value of a property in a property sheet in the
+``portal_properties`` tool::
+
+ self.assertEqual(siteProperties.getProperty('many_users'), True)
+
+Installing products and extension profiles
+------------------------------------------
+
+To apply a particular extension profile::
+
+ from plone.app.testing import applyProfile
+
+ applyProfile(portal, 'my.product:default')
+
+This is the preferred method of installing a product's configuration.
+
+To install an add-on product into the Plone site using the
+``portal_quickinstaller`` tool::
+
+ from plone.app.testing import quickInstallProduct
+
+ quickInstallProduct(portal, 'my.product')
+
+To re-install a product using the quick-installer::
+
+ quickInstallProduct(portal, 'my.product', reinstall=True)
+
+Note that both of these assume the product's ZCML has been loaded, which is
+usually done during layer setup. See the layer examples above for more details
+on how to do that.
+
+When writing a product that has an installation extension profile, it is often
+desirable to write tests that inspect the state of the site after the profile
+has been applied. Some of the more common such tests are shown below.
+
+To verify that a product has been installed (e.g. as a dependency via
+``metadata.xml``)::
+
+ from Products.CMFCore.utils import getToolByName
+
+ quickinstaller = getToolByName(portal, 'portal_quickinstaller')
+ self.assertTrue(quickinstaller.isProductInstalled('my.product'))
+
+To verify that a particular content type has been installed (e.g. via
+``types.xml``)::
+
+ typesTool = getToolByName(portal, 'portal_types')
+
+ self.assertNotEqual(typesTool.getTypeInfo('mytype'), None)
+
+To verify that a new catalog index has been installed (e.g. via
+``catalog.xml``)::
+
+ catalog = getToolByName(portal, 'portal_catalog')
+
+ self.assertTrue('myindex' in catalog.indexes())
+
+To verify that a new catalog metadata column has been added (e.g. via
+``catalog.xml``)::
+
+ self.assertTrue('myattr' in catalog.schema())
+
+To verify that a new workflow has been installed (e.g. via
+``workflows.xml``)::
+
+ workflowTool = getToolByName(portal, 'portal_workflow')
+
+ self.assertNotEqual(workflowTool.getWorkflowById('my_workflow'), None)
+
+To verify that a new workflow has been assigned to a type (e.g. via
+``workflows.xml``)::
+
+ self.assertEqual(dict(workflowTool.listChainOverrides())['mytype'], ('my_workflow',))
+
+To verify that a new workflow has been set as the default (e.g. via
+``workflows.xml``)::
+
+ self.assertEqual(workflowTool.getDefaultChain(), ('my_workflow',))
+
+To test the value of a property in the ``portal_properties`` tool (e.g. set
+via ``propertiestool.xml``):::
+
+ propertiesTool = getToolByName(portal, 'portal_properties')
+ siteProperties = propertiesTool['site_properties']
+
+ self.assertEqual(siteProperties.getProperty('some_property'), "some value")
+
+To verify that a stylesheet has been installed in the ``portal_css`` tool
+(e.g. via ``cssregistry.xml``)::
+
+ cssRegistry = getToolByName(portal, 'portal_css')
+
+ self.assertTrue('mystyles.css' in cssRegistry.getResourceIds())
+
+To verify that a JavaScript resource has been installed in the
+``portal_javascripts`` tool (e.g. via ``jsregistry.xml``)::
+
+ jsRegistry = getToolByName(portal, 'portal_javascripts')
+
+ self.assertTrue('myscript.js' in jsRegistry.getResourceIds())
+
+To verify that a new role has been added (e.g. via ``rolemap.xml``)::
+
+ self.assertTrue('NewRole' in portal.valid_roles())
+
+To verify that a permission has been granted to a given set of roles (e.g. via
+``rolemap.xml``)::
+
+ roles = [r['name'] for r in portal.rolesOfPermission('My Permission') if r['selected']]
+ self.assertEqual(roles, ['Member', 'Manager'])
+
+Traversal
+---------
+
+To traverse to a view, page template or other resource, use
+``restrictedTraverse()`` with a relative path::
+
+ resource = portal.restrictedTraverse('f1/@@folder_contents')
+
+The return value is a view object, page template object, or other resource.
+It may be invoked to obtain an actual response (see below).
+
+``restrictedTraverse()`` performs an explicit security check, and so may
+raise ``Unauthorized`` if the current test user does not have permission to
+view the given resource. If you don't want that, you can use::
+
+ resource = portal.unrestrictedTraverse('f1/@@folder_contents')
+
+You can call this on a folder or other content item as well, to traverse from
+that starting point, e.g. this is equivalent to the first example above::
+
+ f1 = portal['f1']
+ resource = f1.restrictedTraverse('@@folder_contents')
+
+Note that this traversal will not take ``IPublishTraverse`` adapters into
+account, and you cannot pass query string parameters. In fact,
+``restrictedTraverse()`` and ``unrestrictedTraverse()`` implement the type of
+traversal that happens with path expressions in TAL, which is similar, but not
+identical to URL traversal.
+
+To look up a view manually::
+
+ from zope.component import getMultiAdapter
+
+ view = getMultiAdapter((f1, request), name=u"folder_contents")
+
+Note that the name here should not include the ``@@`` prefix.
+
+To simulate an ``IPublishTraverse`` adapter call, presuming the view
+implements ``IPublishTraverse``::
+
+ next = view.IPublishTraverse(request, u"some-name")
+
+Or, if the ``IPublishTraverse`` adapter is separate from the view::
+
+ from zope.publisher.interfaces import IPublishTraverse
+
+ publishTraverse = getMultiAdapter((f1, request), IPublishTraverse)
+ next = view.IPublishTraverse(request, u"some-name")
+
+To simulate a form submission or query string parameters::
+
+ request.form.update({
+ 'name': "John Smith",
+ 'age': 23
+ })
+
+The ``form`` dictionary contains the marshalled request. That is, if you are
+simulating a query string parameter or posted form variable that uses a
+marshaller like ``:int`` (e.g. ``age:int`` in the example above), the value
+in the ``form`` dictionary should be marshalled (an int instead of a string,
+in the example above), and the name should be the base name (``age`` instead
+of ``age:int``).
+
+To invoke a view and obtain the response body as a string::
+
+ view = f1.restrictedTraverse('@@folder_contents')
+ body = view()
+
+ self.assertFalse(u"An unexpected error occurred" in body)
+
+Please note that this approach is not perfect. In particular, the request
+is will not have the right URL or path information. If your view depends on
+this, you can fake it by setting the relevant keys in the request, e.g.::
+
+ request.set('URL', f1.absolute_url() + '/@@folder_contents')
+ request.set('ACTUAL_URL', f1.absolute_url() + '/@@folder_contents')
+
+To inspect the state of the request (e.g. after a view has been invoked)::
+
+ self.assertEqual(request.get('disable_border'), True)
+
+To inspect response headers (e.g. after a view has been invoked)::
+
+ response = request.response
+
+ self.assertEqual(response.getHeader('content-type'), 'text/plain')
+
+Simulating browser interaction
+------------------------------
+
+End-to-end functional tests can use `zope.testbrowser`_ to simulate user
+interaction. This acts as a web browser, connecting to Zope via a special
+channel, making requests and obtaining responses.
+
+ **Note:** zope.testbrowser runs entirely in Python, and does not simulate
+ a JavaScript engine.
+
+Note that to use ``zope.testbrowser``, you need to use one of the functional
+testing layers, e.g. ``PLONE_FUNCTIONAL_TESTING``, or another layer
+instantiated with the ``FunctionalTesting`` class.
+
+If you want to create some initial content, you can do so either in a layer,
+or in the test itself, before invoking the test browser client. In the latter
+case, you need to commit the transaction before it becomes available, e.g.::
+
+ from plone.app.testing import setRoles
+ from plone.app.testing import TEST_USER_ID
+
+ # Make some changes
+ setRoles(portal, TEST_USER_ID, ['Manager'])
+ portal.invokeFactory('Folder', 'f1', title=u"Folder 1")
+ setRoles(portal, TEST_USER_ID, ['Member'])
+
+ # Commit so that the test browser sees these changes
+ import transaction
+ transaction.commit()
+
+To obtain a new test browser client::
+
+ from plone.testing.z2 import Browser
+
+ # This is usually self.app (Zope root) or site.portal (test Plone site root)
+ browser = Browser(app)
+
+To open a given URL::
+
+ portalURL = portal.absolute_url()
+ browser.open(portalURL)
+
+To inspect the response::
+
+ self.assertTrue(u"Welcome" in browser.contents)
+
+To inspect response headers::
+
+ self.assertEqual(browser.headers['content-type'], 'text/html; charset=utf-8')
+
+To follow a link::
+
+ browser.getLink('Edit').click()
+
+This gets a link by its text. To get a link by HTML id::
+
+ browser.getLink(id='edit-link').click()
+
+To verify the current URL::
+
+ self.assertEqual(portalURL + '/edit', browser.url)
+
+To set a form control value::
+
+ browser.getControl('Age').value = u"30"
+
+This gets the control by its associated label. To get a control by its form
+variable name::
+
+ browser.getControl(name='age:int').value = u"30"
+
+See the `zope.testbrowser`_ documentation for more details on how to select
+and manipulate various types of controls.
+
+To submit a form by clicking a button::
+
+ browser.getControl('Save').click()
+
+Again, this uses the label to find the control. To use the form variable
+name::
+
+ browser.getControl(name='form.button.Save').click()
+
+To simulate HTTP BASIC authentication and remain logged in for all
+requests::
+
+ from plone.app.testing import TEST_USER_NAME, TEST_USER_PASSWORD
+
+ browser.addHeader('Authorization', 'Basic %s:%s' % (TEST_USER_NAME, TEST_USER_PASSWORD,))
+
+To simulate logging in via the login form::
+
+ browser.open(portalURL + '/login_form')
+ browser.getControl(name='__ac_name').value = TEST_USER_NAME
+ browser.getControl(name='__ac_password').value = TEST_USER_PASSWORD
+ browser.getControl(name='submit').click()
+
+To simulate logging out::
+
+ browser.open(portalURL + '/logout')
+
+Debugging tips
+~~~~~~~~~~~~~~
+
+By default, only HTTP error codes (e.g. 500 Server Side Error) are shown when
+an error occurs on the server. To see more details, set ``handleErrors`` to
+False::
+
+ browser.handleErrors = False
+
+To inspect the error log and obtain a full traceback of the latest entry::
+
+ from Products.CMFCore.utils import getToolByName
+
+ errorLog = getToolByName(portal, 'error_log')
+ print errorLog.getLogEntries()[-1]['tb_text']
+
+To save the current response to an HTML file::
+
+ open('/tmp/testbrowser.html', 'w').write(browser.contents)
+
+You can now open this file and use tools like Firebug to inspect the structure
+of the page. You should remove the file afterwards.
+
+Comparison with ZopeTestCase/PloneTestCase
+==========================================
+
+`plone.testing`_ and ``plone.app.testing`` have in part evolved from
+``ZopeTestCase``, which ships with Zope 2 in the ``Testing`` package, and
+`Products.PloneTestCase`_, which ships with Plone and is used by Plone itself
+as well as numerous add-on products.
+
+If you are familiar with ``ZopeTestCase`` and ``PloneTestCase``, the concepts
+of these package should be familiar to you. However, there are some important
+differences to bear in mind.
+
+* ``plone.testing`` and ``plone.app.testing`` are unburdened by the legacy
+ support that ``ZopeTestCase`` and ``PloneTestCase`` have to include. This
+ makes them smaller and easier to understand and maintain.
+
+* Conversely, ``plone.testing`` only works with Python 2.6 and Zope 2.12 and
+ later. ``plone.app.testing`` only works with Plone 4 and later. If you need
+ to write tests that run against older versions of Plone, you'll need to use
+ ``PloneTestCase``.
+
+* ``ZopeTestCase``/``PloneTestCase`` were written before layers were available
+ as a setup mechanism. ``plone.testing`` is very layer-oriented.
+
+* ``PloneTestCase`` provides a base class, also called ``PloneTestCase``,
+ which you must use, as it performs setup and tear-down. ``plone.testing``
+ moves shared state to layers and layer resources, and does not impose any
+ particular base class for tests. This does sometimes mean a little more
+ typing (e.g. ``self.layer['portal']`` vs. ``self.portal``), but it makes
+ it much easier to control and re-use test fixtures. It also makes your
+ test code simpler and more explicit.
+
+* ``ZopeTestCase`` has an ``installProduct()`` function and a corresponding
+ ``installPackage()`` function. `plone.testing`_ has only an
+ ``installProduct()``, which can configure any kind of Zope 2 product (i.e.
+ packages in the ``Products.*`` namespace, old-style products in a special
+ ``Products`` folder, or packages in any namespace that have had their ZCML
+ loaded and which include a ``<five:registerPackage />`` directive in their
+ configuration). Note that you must pass a full dotted name to this function,
+ even for "old-style" products in the ``Products.*`` namespace, e.g.
+ ``Products.LinguaPlone`` instead of ``LinguaPlone``.
+
+* On setup, ``PloneTestCase`` will load Zope 2's default ``site.zcml``. This
+ in turn will load all ZCML for all packages in the ``Products.*`` namespace.
+ ``plone.testing`` does not do this (and you are strongly encouraged from
+ doing it yourself), because it is easy to accidentally include packages in
+ your fixture that you didn't intend to be there (and which can actually
+ change the fixture substantially). You should load your package's ZCML
+ explicitly. See the `plone.testing`_ documentation for details.
+
+* When using ``PloneTestCase``, any package that has been loaded onto
+ ``sys.path`` and which defines the ``z3c.autoinclude.plugin:plone`` entry
+ point will be loaded via `z3c.autoinclude`_'s plugin mechanism. This loading
+ is explicitly disabled, for the same reasons that the ``Products.*`` auto-
+ loading is. You should load your packages' configuration explicitly.
+
+* ``PloneTestCase`` sets up a basic fixture that has member folder enabled,
+ and in which the test user's member folder is available as ``self.folder``.
+ The ``plone_workflow`` workflow is also installed as the default.
+ ``plone.app.testing`` takes a more minimalist approach. To create a test
+ folder owned by the test user that is similar to ``self.folder`` in a
+ ``PloneTestCase``, you can do::
+
+ import unittest2 as unittest
+ from plone.app.testing import TEST_USER_ID, setRoles
+ from plone.app.testing import PLONE_INTEGRATION_TESTING
+
+ class MyTest(unitest.TestCase):
+
+ layer = PLONE_INTEGRATION_TESTING
+
+ def setUp(self):
+ self.portal = self.layer['portal']
+
+ setRoles(self.portal, TEST_USER_ID, ['Manager'])
+ self.portal.invokeFactory('Folder', 'test-folder')
+ setRoles(self.portal, TEST_USER_ID, ['Member'])
+
+ self.folder = self.portal['test-folder']
+
+ You could of course do this type of setup in your own layer and expose it
+ as a resource instead.
+
+* To use `zope.testbrowser`_ with ``PloneTestCase``, you should use its
+ ``FunctionalTestCase`` as a base class, and then use the following pattern::
+
+ from Products.Five.testbrowser import Browser
+ browser = Browser()
+
+ The equivalent pattern in ``plone.app.testing`` is to use the
+ ``FunctionalTesting`` test lifecycle layer (see example above), and then
+ use::
+
+ from plone.testing.z2 import Browser
+ browser = Browser(self.layer['app'])
+
+ Also note that if you have made changes to the fixture prior to calling
+ ``browser.open()``, they will *not* be visible until you perform an
+ explicit commit. See the ``zope.testbrowser`` examples above for details.
+
+.. _plone.testing: http://pypi.python.org/pypi/plone.testing
+.. _zope.testing: http://pypi.python.org/pypi/zope.testing
+.. _z3c.autoinclude: http://pypi.python.org/pypi/z3c.autoinclude
+.. _zope.testbrowser: http://pypi.python.org/pypi/zope.testbrowser
+.. _Products.PloneTestCase: http://pypi.python.org/pypi/Products.PloneTestCase
diff --git a/setup.py b/setup.py
index 91a332a..996f53e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,5 @@
from setuptools import setup, find_packages
+import os
version = '5.0a1'
@@ -16,12 +17,18 @@
'decorator',
'selenium']
+def read(*rnames):
+ return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
+
+long_description = \
+ read('docs', 'source','README.rst') + \
+ read('CHANGES.rst')
+
setup(
name='plone.app.testing',
version=version,
description="Testing tools for Plone-the-application, based on plone.testing.",
- long_description=open("README.rst").read() + "\n" +
- open("CHANGES.rst").read(),
+ long_description=long_description,
classifiers=[
"Environment :: Web Environment",
"Framework :: Plone",
-------------------------------------------------------------------------------
-------------- next part --------------
A non-text attachment was scrubbed...
Name: CHANGES.log
Type: application/octet-stream
Size: 113506 bytes
Desc: not available
URL: <http://lists.plone.org/pipermail/plone-testbot/attachments/20140330/7761a4dd/attachment-0002.obj>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: build.log
Type: application/octet-stream
Size: 52752 bytes
Desc: not available
URL: <http://lists.plone.org/pipermail/plone-testbot/attachments/20140330/7761a4dd/attachment-0003.obj>
More information about the Testbot
mailing list