Plone自动化测试手册
Plone自动化测试手册
http://www.315ok.org/blogfolder/398
http://www.315ok.org/logo.png
Plone自动化测试手册
Plone自动化测试手册
Layer referenceThis 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 oflayer instances. It is important to realise that these layers all have thesame fundamental fixture: they just manage test setup and tear-downdifferently.
When set up, the fixture will:
[table]
[tr][td]Constant[/td][td]Purpose[/td][/tr]
[tr][td]PLONE_SITE_ID[/td][td]The id of the Plone site object inside the Zopeapplication root.[/td][/tr]
[tr][td]PLONE_SITE_TITLE[/td][td]The title of the Plone site[/td][/tr]
[tr][td]DEFAULT_LANGUAGE[/td][td]The default language of the Plone site ('en')[/td][/tr]
[tr][td]TEST_USER_ID[/td][td]The id of the test user[/td][/tr]
[tr][td]TEST_USER_NAME[/td][td]The username of the test user[/td][/tr]
[tr][td]TEST_USER_PASSWORD[/td][td]The password of the test user[/td][/tr]
[tr][td]TEST_USER_ROLES[/td][td]The default global roles of the test user -('Member',)[/td][/tr]
[tr][td]SITE_OWNER_NAME[/td][td]The username of the user owning the Plone site.[/td][/tr]
[tr][td]SITE_OWNER_PASSWORD[/td][td]The password of the user owning the Plone site.[/td][/tr]
[/table]All the layers also expose a resource in addition to those from theirbase layers, made available during tests:
portalThe Plone site root.Plone site fixture[table]
[tr][td]Layer:[/td][td]plone.app.testing.PLONE_FIXTURE[/td][/tr]
[tr][td]Class:[/td][td]plone.app.testing.layers.PloneFixture[/td][/tr]
[tr][td]Bases:[/td][td]plone.testing.z2.STARTUP[/td][/tr]
[tr][td]Resources:[/td][td] [/td][/tr]
[/table]This layer sets up the Plone site fixture on top of the z2.STARTUPfixture.
You should not use this layer directly, as it does not provide any testlifecycle or transaction management. Instead, you should use a layercreated with either the IntegrationTesting or FunctionalTestingclasses, as outlined below.
Integration and functional testing test lifecyclesplone.app.testing comes with two layer classes, IntegrationTestingand FunctionalTesting, which derive from the corresponding layer classesin plone.testing.z2.
These classes set up the app, request and portal resources, andreset the fixture (including various global caches) between each test run.
As with the classes in plone.testing, the IntegrationTesting classwill create a new transaction for each test and roll it back on test tear-down, which is efficient for integration testing, whilst FunctionalTestingwill 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 layerclass that has PLONE_FIXTURE as its default base, instantiating that as aseparate "fixture" layer. This layer is not to be used in tests directly,since it won't have test/transaction lifecycle management, but represents ashared fixture, potentially for both functional and integration testing. Itis 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 usingthe 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[table]
[tr][td]Layer:[/td][td]plone.app.testing.PLONE_INTEGRATION_TESTING[/td][/tr]
[tr][td]Class:[/td][td]plone.app.testing.layers.IntegrationTesting[/td][/tr]
[tr][td]Bases:[/td][td]plone.app.testing.PLONE_FIXTURE[/td][/tr]
[tr][td]Resources:[/td][td]portal (test setup only)[/td][/tr]
[/table]This layer can be used for integration testing against the basicPLONE_FIXTURE layer.
You can use this directly in your tests if you do not need to set up anyother shared fixture.
However, you would normally not extend this layer - see above.
Plone functional testing[table]
[tr][td]Layer:[/td][td]plone.app.testing.PLONE_FUNCTIONAL_TESTING[/td][/tr]
[tr][td]Class:[/td][td]plone.app.testing.layers.FunctionalTesting[/td][/tr]
[tr][td]Bases:[/td][td]plone.app.testing.PLONE_FIXTURE[/td][/tr]
[tr][td]Resources:[/td][td]portal (test setup only)[/td][/tr]
[/table]This layer can be used for functional testing against the basicPLONE_FIXTURE layer, for example using zope.testbrowser.
You can use this directly in your tests if you do not need to set up anyother shared fixture.
Again, you would normally not extend this layer - see above.
Plone ZServer[table]
[tr][td]Layer:[/td][td]plone.app.testing.PLONE_ZSERVER[/td][/tr]
[tr][td]Class:[/td][td]plone.testing.z2.ZServer[/td][/tr]
[tr][td]Bases:[/td][td]plone.app.testing.PLONE_FUNCTIONAL_TESTING[/td][/tr]
[tr][td]Resources:[/td][td]portal (test setup only)[/td][/tr]
[/table]This is layer is intended for functional testing using a live, running HTTPserver, e.g. using Selenium or Windmill.
Again, you would not normally extend this layer. To create a custom layerthat 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.testingfor further details.
Plone FTP server[table]
[tr][td]Layer:[/td][td]plone.app.testing.PLONE_FTP_SERVER[/td][/tr]
[tr][td]Class:[/td][td]plone.app.testing.layers.FunctionalTesting[/td][/tr]
[tr][td]Bases:[/td][td]plone.app.testing.PLONE_FIXTUREplone.testing.z2.ZSERVER_FIXTURE[/td][/tr]
[tr][td]Resources:[/td][td]portal (test setup only)[/td][/tr]
[/table]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.testingfor further details.
Helper functionsA number of helper functions are provided for use in tests and custom layers.
Plone site context managerploneSite(db=None, connection=None, environ=None)[align=left]Use this context manager to access and make changes to the Plone siteduring layer setup. In most cases, you will use it without arguments,but if you have special needs, you can tie it to a particular databaseinstance. See the description of the zopeApp() context manager inplone.testing (which this context manager uses internally) for details.[/align]The usual pattern is to call it during setUp() or tearDown() inyour 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 beforeentering 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 ploneSiteblock. Because the local site is set to the Plone site, you may end upaccidentally registering components in the local site manager, which cancause pickling errors later.
Note: You should not use this in a test, or in a testSetUp() ortestTearDown() method of a layer based on one of the layer in thispackage. Use the portal resource instead.
[align=left]Also note: If you are writing a layer setting up a Plone site fixture,you may want to use the PloneSandboxLayer layer base class, andimplement the setUpZope(), setUpPloneSite(), tearDownZope()and/or tearDownPloneSite() methods instead. See below.[/align]
User managementlogin(portal, userName)[align=left]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).[/align]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()[align=left]Simulate logging out, i.e. becoming the anonymous user. This is equivalentto the z2.logout() helper in plone.testing.[/align]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)[align=left]Set the roles for the given user. roles is a list of roles.[/align]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 installationapplyProfile(portal, profileName)[align=left]Install a GenericSetup profile (usually an extension profile) by name,using the portal_setup tool. The name is normally made up of a packagename and a profile name. Do not use the profile- prefix.[/align]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)[align=left]Use this function to install a particular product into the given Plonesite, using the portal_quickinstaller tool. If reinstall isFalse and the product is already installed, nothing will happen; ifreinstall is True, the product will be reinstalled. TheproductName should be a full dotted name, e.g. Products.MyProduct,or my.product.[/align]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 sandboxingpushGlobalRegistry(portal, new=None, name=None)[align=left]Create or obtain a stack of global component registries, and push a newregistry to the top of the stack. This allows Zope Component Architectureregistrations (e.g. loaded via ZCML) to be effectively torn down.[/align]If you are going to use this function, please read the correspondingdocumentation for zca.pushGlobalRegistry() in plone.testing. Inparticular, note that you must reciprocally call popGlobalRegistry()(see below).
This helper is based on zca.pushGlobalRegistry(), but will also fixup the local component registry in the Plone site portal so that ithas 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)[align=left]Tear down the top of the component architecture stack, as created withpushGlobalRegistry()[/align]For example:
…
def tearDown(self):
with ploneSite() as portal:
popGlobalRegistry(portal)
Global state cleanuptearDownMultiPluginRegistration(pluginName)[align=left]PluggableAuthService "MultiPlugins" are kept in a global registry. Ifyou 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.[/align]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 classIf you are writing a custom layer to test your own Plone add-on product, youwill often want to do the following on setup:
On tear-down, you will then want to:
Stacking a demo storage and component registry is the safest way to avoidfixtures bleeding between tests. However, it can be tricky to ensure thateverything 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() andtearDown() for you. You simply have to override one or more of thefollowing 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 helperplone.testing.z2.installProduct(), or manipulate other global state.setUpPloneSite(self, portal)This is called during setup. portal is the Plone site root asconfigured by the ploneSite() context manager. Use this to makepersistent changes inside the Plone site, such as installing productsusing the applyProfile() or quickInstallProduct() helpers, orsetting up default content.tearDownZope(self, app)[align=left]This is called during tear-down, before the global component registry andstacked DemoStorage are popped. Use this to tear down any additionalglobal state.[/align][align=left]Note: Global component registrations PAS multi-plugin registrations areautomatically torn down. Product installations are not, so you should usethe uninstallProduct() helper if any products were installed duringsetUpZope().[/align]tearDownPloneSite(self, portal)[align=left]This is called during tear-down, before the global component registry andstacked DemoStorage are popped. During this method, the localcomponent site hook is set, giving you access to local components.[/align][align=left]Note: Persistent changes to the ZODB are automatically torn down byvirtue of a stacked DemoStorage. Thus, this method is less commonlyused than the others described here.[/align]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 filethat loads some components and registers a GenericSetup profile, making itinstallable in the Plone site. On layer setup, we want to load the product'sconfiguration and install it into the Plone site.
The layer would conventionally live in a module testing.py at the root ofthe 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 canuse this as a base if they want to build on this fixture, but it would notbe used in tests directly. For that, we have crated an IntegrationTestinginstance, MY_PRODUCT_INTEGRATION_TESTING.
Of course, we could have created a FunctionalTesting instance aswell, 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 saythe product had a content type 'my.product.page' and we wanted to create sometest 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 testuser the Manager role temporarily, to allow object construction (whichperforms an explicit permission check).
[indent]Note: Automatic tear down suffices for all the test setup above. Ifthe 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 atearDownPloneSite() method to perform the necessary cleanup.[/indent]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 runtests and assertions.
Common test patternsplone.testing's documentation contains details about the fundamentaltechniques for writing tests of various kinds. In a Plone context, however,some patterns tend to crop up time and again. Below, we will attempt tocatalogue 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 mayalso be useful in layer set-up/tear-down. We have used unittest syntaxhere, 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 orFunctionalTesting classes as shown above.
We will also assume that the variables app, portal and request aredefined 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 fromplone.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. Ifa given import or variable is used more than once in the same section, itwill only be shown once.
Basic content managementTo create a content item of type 'Folder' with the id 'f1' in the root ofthe portal:
portal.invokeFactory('Folder', 'f1', title=u"Folder 1")
The title argument is optional. Other basic properties, likedescription, can be set as well.
Note that this may fail with an Unauthorized exception, since the testuser won't normally have permissions to add content in the portal root, andthe invokeFactory() method performs an explicit security check. You canset the roles of the test user to ensure that he has the necessarypermissions:
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:
[indent]del portal['f1'][/indent]
SearchingTo 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 cancall len() on it to get the number of search results, or iterate throughit. The items in the list are catalog brains. They have attributes thatcorrespond to the "metadata" columns configured for the catalog, e.g.Title, Description, etc. Note that these are simple attributes (notmethods), and contain the value of the corresponding attribute or method fromthe source object at the time the object was cataloged (i.e. they are notnecessarily 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 managementTo 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 testingenvironment 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-worldscenarios):
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 rolesTo get a user's roles in a particular context (taking local roles intoaccount):
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 theportal 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 theportal:
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 thecontext 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())
WorkflowTo 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_workflowtool supports longer chains, where an item is subject to more than oneworkflow 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 amulti-workflow chain, separate workflow names by commas. To indicate that atype 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 thetransaction 'publish':
workflowTool.doActionFor(f1, 'publish')
Note that this performs an explicit permission check, so if the current userdoesn't have permission to perform this workflow action, you may get an errorindicating the action is not available. If so, use login() orsetRoles() to ensure the current user is able to change the workflowstate.
To check the current workflow state of the content object f1:
self.assertEqual(workflowTool.getInfoFor(f1, 'review_state'), 'published')
PropertiesTo 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 theportal_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 theportal_properties tool:
self.assertEqual(siteProperties.getProperty('many_users'), True)
Installing products and extension profilesTo 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 theportal_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 isusually done during layer setup. See the layer examples above for more detailson how to do that.
When writing a product that has an installation extension profile, it is oftendesirable to write tests that inspect the state of the site after the profilehas 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 viametadata.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. viatypes.xml):
typesTool = getToolByName(portal, 'portal_types')
self.assertNotEqual(typesTool.getTypeInfo('mytype'), None)
To verify that a new catalog index has been installed (e.g. viacatalog.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. viacatalog.xml):
self.assertTrue('myattr' in catalog.schema())
To verify that a new workflow has been installed (e.g. viaworkflows.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. viaworkflows.xml):
self.assertEqual(dict(workflowTool.listChainOverrides())['mytype'], ('my_workflow',))
To verify that a new workflow has been set as the default (e.g. viaworkflows.xml):
self.assertEqual(workflowTool.getDefaultChain(), ('my_workflow',))
To test the value of a property in the portal_properties tool (e.g. setvia 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 theportal_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. viarolemap.xml):
roles = [r['name'] for r in portal.rolesOfPermission('My Permission') if r['selected']]
self.assertEqual(roles, ['Member', 'Manager'])
TraversalTo traverse to a view, page template or other resource, userestrictedTraverse() 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 mayraise Unauthorized if the current test user does not have permission toview 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 fromthat 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 intoaccount, and you cannot pass query string parameters. In fact,restrictedTraverse() and unrestrictedTraverse() implement the type oftraversal that happens with path expressions in TAL, which is similar, but notidentical 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 viewimplements 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 aresimulating a query string parameter or posted form variable that uses amarshaller like :int (e.g. age:int in the example above), the valuein 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 insteadof 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 requestis will not have the right URL or path information. If your view depends onthis, 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 interactionEnd-to-end functional tests can use zope.testbrowser to simulate userinteraction. This acts as a web browser, connecting to Zope via a specialchannel, making requests and obtaining responses.
[indent]Note: zope.testbrowser runs entirely in Python, and does not simulatea JavaScript engine.[/indent]Note that to use zope.testbrowser, you need to use one of the functionaltesting layers, e.g. PLONE_FUNCTIONAL_TESTING, or another layerinstantiated 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 lattercase, 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
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 formvariable name:
browser.getControl(name='age:int').value = u"30"
See the zope.testbrowser documentation for more details on how to selectand 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 variablename:
browser.getControl(name='form.button.Save').click()
To simulate HTTP BASIC authentication and remain logged in for allrequests:
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 tipsBy default, only HTTP error codes (e.g. 500 Server Side Error) are shown whenan error occurs on the server. To see more details, set handleErrors toFalse:
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 structureof the page. You should remove the file afterwards.
When set up, the fixture will:
- Create a ZODB sandbox, via a stacked DemoStorage. This ensurespersistent changes made during layer setup can be cleanly torn down.
- Configure a global component registry sandbox. This ensures that globalcomponent registrations (e.g. as a result of loading ZCML configuration)can be cleanly torn down.
- Create a configuration context with the disable-autoinclude featureset. This has the effect of stopping Plone from automatically loading theconfiguration of any installed package that uses thez3c.autoinclude.plugin:plone entry point via z3c.autoinclude. (Thisis to avoid accidentally polluting the test fixture - custom layers shouldload 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 inturn pulls in the configuration for the core of Plone.
- Create a default Plone site, with the default theme enabled, but with nodefault content.
- Add a user to the root user folder with the Manager role.
- Add a test user to this instance with the Member role.
- The test user is logged in
- The local component site is set
- Various global caches are cleaned up
[table]
[tr][td]Constant[/td][td]Purpose[/td][/tr]
[tr][td]PLONE_SITE_ID[/td][td]The id of the Plone site object inside the Zopeapplication root.[/td][/tr]
[tr][td]PLONE_SITE_TITLE[/td][td]The title of the Plone site[/td][/tr]
[tr][td]DEFAULT_LANGUAGE[/td][td]The default language of the Plone site ('en')[/td][/tr]
[tr][td]TEST_USER_ID[/td][td]The id of the test user[/td][/tr]
[tr][td]TEST_USER_NAME[/td][td]The username of the test user[/td][/tr]
[tr][td]TEST_USER_PASSWORD[/td][td]The password of the test user[/td][/tr]
[tr][td]TEST_USER_ROLES[/td][td]The default global roles of the test user -('Member',)[/td][/tr]
[tr][td]SITE_OWNER_NAME[/td][td]The username of the user owning the Plone site.[/td][/tr]
[tr][td]SITE_OWNER_PASSWORD[/td][td]The password of the user owning the Plone site.[/td][/tr]
[/table]All the layers also expose a resource in addition to those from theirbase layers, made available during tests:
portalThe Plone site root.Plone site fixture[table]
[tr][td]Layer:[/td][td]plone.app.testing.PLONE_FIXTURE[/td][/tr]
[tr][td]Class:[/td][td]plone.app.testing.layers.PloneFixture[/td][/tr]
[tr][td]Bases:[/td][td]plone.testing.z2.STARTUP[/td][/tr]
[tr][td]Resources:[/td][td] [/td][/tr]
[/table]This layer sets up the Plone site fixture on top of the z2.STARTUPfixture.
You should not use this layer directly, as it does not provide any testlifecycle or transaction management. Instead, you should use a layercreated with either the IntegrationTesting or FunctionalTestingclasses, as outlined below.
Integration and functional testing test lifecyclesplone.app.testing comes with two layer classes, IntegrationTestingand FunctionalTesting, which derive from the corresponding layer classesin plone.testing.z2.
These classes set up the app, request and portal resources, andreset the fixture (including various global caches) between each test run.
As with the classes in plone.testing, the IntegrationTesting classwill create a new transaction for each test and roll it back on test tear-down, which is efficient for integration testing, whilst FunctionalTestingwill 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 layerclass that has PLONE_FIXTURE as its default base, instantiating that as aseparate "fixture" layer. This layer is not to be used in tests directly,since it won't have test/transaction lifecycle management, but represents ashared fixture, potentially for both functional and integration testing. Itis 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 usingthe 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[table]
[tr][td]Layer:[/td][td]plone.app.testing.PLONE_INTEGRATION_TESTING[/td][/tr]
[tr][td]Class:[/td][td]plone.app.testing.layers.IntegrationTesting[/td][/tr]
[tr][td]Bases:[/td][td]plone.app.testing.PLONE_FIXTURE[/td][/tr]
[tr][td]Resources:[/td][td]portal (test setup only)[/td][/tr]
[/table]This layer can be used for integration testing against the basicPLONE_FIXTURE layer.
You can use this directly in your tests if you do not need to set up anyother shared fixture.
However, you would normally not extend this layer - see above.
Plone functional testing[table]
[tr][td]Layer:[/td][td]plone.app.testing.PLONE_FUNCTIONAL_TESTING[/td][/tr]
[tr][td]Class:[/td][td]plone.app.testing.layers.FunctionalTesting[/td][/tr]
[tr][td]Bases:[/td][td]plone.app.testing.PLONE_FIXTURE[/td][/tr]
[tr][td]Resources:[/td][td]portal (test setup only)[/td][/tr]
[/table]This layer can be used for functional testing against the basicPLONE_FIXTURE layer, for example using zope.testbrowser.
You can use this directly in your tests if you do not need to set up anyother shared fixture.
Again, you would normally not extend this layer - see above.
Plone ZServer[table]
[tr][td]Layer:[/td][td]plone.app.testing.PLONE_ZSERVER[/td][/tr]
[tr][td]Class:[/td][td]plone.testing.z2.ZServer[/td][/tr]
[tr][td]Bases:[/td][td]plone.app.testing.PLONE_FUNCTIONAL_TESTING[/td][/tr]
[tr][td]Resources:[/td][td]portal (test setup only)[/td][/tr]
[/table]This is layer is intended for functional testing using a live, running HTTPserver, e.g. using Selenium or Windmill.
Again, you would not normally extend this layer. To create a custom layerthat 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.testingfor further details.
Plone FTP server[table]
[tr][td]Layer:[/td][td]plone.app.testing.PLONE_FTP_SERVER[/td][/tr]
[tr][td]Class:[/td][td]plone.app.testing.layers.FunctionalTesting[/td][/tr]
[tr][td]Bases:[/td][td]plone.app.testing.PLONE_FIXTUREplone.testing.z2.ZSERVER_FIXTURE[/td][/tr]
[tr][td]Resources:[/td][td]portal (test setup only)[/td][/tr]
[/table]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.testingfor further details.
Helper functionsA number of helper functions are provided for use in tests and custom layers.
Plone site context managerploneSite(db=None, connection=None, environ=None)[align=left]Use this context manager to access and make changes to the Plone siteduring layer setup. In most cases, you will use it without arguments,but if you have special needs, you can tie it to a particular databaseinstance. See the description of the zopeApp() context manager inplone.testing (which this context manager uses internally) for details.[/align]The usual pattern is to call it during setUp() or tearDown() inyour 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 beforeentering 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 ploneSiteblock. Because the local site is set to the Plone site, you may end upaccidentally registering components in the local site manager, which cancause pickling errors later.
Note: You should not use this in a test, or in a testSetUp() ortestTearDown() method of a layer based on one of the layer in thispackage. Use the portal resource instead.
[align=left]Also note: If you are writing a layer setting up a Plone site fixture,you may want to use the PloneSandboxLayer layer base class, andimplement the setUpZope(), setUpPloneSite(), tearDownZope()and/or tearDownPloneSite() methods instead. See below.[/align]
User managementlogin(portal, userName)[align=left]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).[/align]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()[align=left]Simulate logging out, i.e. becoming the anonymous user. This is equivalentto the z2.logout() helper in plone.testing.[/align]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)[align=left]Set the roles for the given user. roles is a list of roles.[/align]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 installationapplyProfile(portal, profileName)[align=left]Install a GenericSetup profile (usually an extension profile) by name,using the portal_setup tool. The name is normally made up of a packagename and a profile name. Do not use the profile- prefix.[/align]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)[align=left]Use this function to install a particular product into the given Plonesite, using the portal_quickinstaller tool. If reinstall isFalse and the product is already installed, nothing will happen; ifreinstall is True, the product will be reinstalled. TheproductName should be a full dotted name, e.g. Products.MyProduct,or my.product.[/align]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 sandboxingpushGlobalRegistry(portal, new=None, name=None)[align=left]Create or obtain a stack of global component registries, and push a newregistry to the top of the stack. This allows Zope Component Architectureregistrations (e.g. loaded via ZCML) to be effectively torn down.[/align]If you are going to use this function, please read the correspondingdocumentation for zca.pushGlobalRegistry() in plone.testing. Inparticular, note that you must reciprocally call popGlobalRegistry()(see below).
This helper is based on zca.pushGlobalRegistry(), but will also fixup the local component registry in the Plone site portal so that ithas 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)[align=left]Tear down the top of the component architecture stack, as created withpushGlobalRegistry()[/align]For example:
…
def tearDown(self):
with ploneSite() as portal:
popGlobalRegistry(portal)
Global state cleanuptearDownMultiPluginRegistration(pluginName)[align=left]PluggableAuthService "MultiPlugins" are kept in a global registry. Ifyou 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.[/align]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 classIf you are writing a custom layer to test your own Plone add-on product, youwill often want to do the following on setup:
- Stack a new DemoStorage on top of the one from the base layer. Thisensures that any persistent changes performed during layer setup can betorn down completely, simply by popping the demo storage.
- Stack a new ZCML configuration context. This keeps separate the informationabout which ZCML files were loaded, in case other, independent layers wantto load those same files after this layer has been torn down.
- Push a new global component registry. This allows you to registercomponents (e.g. by loading ZCML or using the test API fromzope.component) and tear down those registration easily by popping thecomponent registry.
- Load your product's ZCML configuration
- Install the product into the test fixture Plone site
On tear-down, you will then want to:
- Remove any Pluggable Authentication Service "multi-plugins" that were addedto the global registry during setup.
- Pop the global component registry to unregister components loaded via ZCML.
- Pop the configuration context resource to restore its state.
- Pop the DemoStorage to undo any persistent changes.
Stacking a demo storage and component registry is the safest way to avoidfixtures bleeding between tests. However, it can be tricky to ensure thateverything 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() andtearDown() for you. You simply have to override one or more of thefollowing 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 helperplone.testing.z2.installProduct(), or manipulate other global state.setUpPloneSite(self, portal)This is called during setup. portal is the Plone site root asconfigured by the ploneSite() context manager. Use this to makepersistent changes inside the Plone site, such as installing productsusing the applyProfile() or quickInstallProduct() helpers, orsetting up default content.tearDownZope(self, app)[align=left]This is called during tear-down, before the global component registry andstacked DemoStorage are popped. Use this to tear down any additionalglobal state.[/align][align=left]Note: Global component registrations PAS multi-plugin registrations areautomatically torn down. Product installations are not, so you should usethe uninstallProduct() helper if any products were installed duringsetUpZope().[/align]tearDownPloneSite(self, portal)[align=left]This is called during tear-down, before the global component registry andstacked DemoStorage are popped. During this method, the localcomponent site hook is set, giving you access to local components.[/align][align=left]Note: Persistent changes to the ZODB are automatically torn down byvirtue of a stacked DemoStorage. Thus, this method is less commonlyused than the others described here.[/align]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 filethat loads some components and registers a GenericSetup profile, making itinstallable in the Plone site. On layer setup, we want to load the product'sconfiguration and install it into the Plone site.
The layer would conventionally live in a module testing.py at the root ofthe 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 canuse this as a base if they want to build on this fixture, but it would notbe used in tests directly. For that, we have crated an IntegrationTestinginstance, MY_PRODUCT_INTEGRATION_TESTING.
Of course, we could have created a FunctionalTesting instance aswell, 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 saythe product had a content type 'my.product.page' and we wanted to create sometest 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 testuser the Manager role temporarily, to allow object construction (whichperforms an explicit permission check).
[indent]Note: Automatic tear down suffices for all the test setup above. Ifthe 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 atearDownPloneSite() method to perform the necessary cleanup.[/indent]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 runtests and assertions.
Common test patternsplone.testing's documentation contains details about the fundamentaltechniques for writing tests of various kinds. In a Plone context, however,some patterns tend to crop up time and again. Below, we will attempt tocatalogue 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 mayalso be useful in layer set-up/tear-down. We have used unittest syntaxhere, 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 orFunctionalTesting classes as shown above.
We will also assume that the variables app, portal and request aredefined 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 fromplone.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. Ifa given import or variable is used more than once in the same section, itwill only be shown once.
Basic content managementTo create a content item of type 'Folder' with the id 'f1' in the root ofthe portal:
portal.invokeFactory('Folder', 'f1', title=u"Folder 1")
The title argument is optional. Other basic properties, likedescription, can be set as well.
Note that this may fail with an Unauthorized exception, since the testuser won't normally have permissions to add content in the portal root, andthe invokeFactory() method performs an explicit security check. You canset the roles of the test user to ensure that he has the necessarypermissions:
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:
[indent]del portal['f1'][/indent]
SearchingTo 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 cancall len() on it to get the number of search results, or iterate throughit. The items in the list are catalog brains. They have attributes thatcorrespond to the "metadata" columns configured for the catalog, e.g.Title, Description, etc. Note that these are simple attributes (notmethods), and contain the value of the corresponding attribute or method fromthe source object at the time the object was cataloged (i.e. they are notnecessarily 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 managementTo 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 testingenvironment 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-worldscenarios):
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 rolesTo get a user's roles in a particular context (taking local roles intoaccount):
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 theportal 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 theportal:
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 thecontext 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())
WorkflowTo 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_workflowtool supports longer chains, where an item is subject to more than oneworkflow 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 amulti-workflow chain, separate workflow names by commas. To indicate that atype 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 thetransaction 'publish':
workflowTool.doActionFor(f1, 'publish')
Note that this performs an explicit permission check, so if the current userdoesn't have permission to perform this workflow action, you may get an errorindicating the action is not available. If so, use login() orsetRoles() to ensure the current user is able to change the workflowstate.
To check the current workflow state of the content object f1:
self.assertEqual(workflowTool.getInfoFor(f1, 'review_state'), 'published')
PropertiesTo 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 theportal_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 theportal_properties tool:
self.assertEqual(siteProperties.getProperty('many_users'), True)
Installing products and extension profilesTo 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 theportal_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 isusually done during layer setup. See the layer examples above for more detailson how to do that.
When writing a product that has an installation extension profile, it is oftendesirable to write tests that inspect the state of the site after the profilehas 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 viametadata.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. viatypes.xml):
typesTool = getToolByName(portal, 'portal_types')
self.assertNotEqual(typesTool.getTypeInfo('mytype'), None)
To verify that a new catalog index has been installed (e.g. viacatalog.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. viacatalog.xml):
self.assertTrue('myattr' in catalog.schema())
To verify that a new workflow has been installed (e.g. viaworkflows.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. viaworkflows.xml):
self.assertEqual(dict(workflowTool.listChainOverrides())['mytype'], ('my_workflow',))
To verify that a new workflow has been set as the default (e.g. viaworkflows.xml):
self.assertEqual(workflowTool.getDefaultChain(), ('my_workflow',))
To test the value of a property in the portal_properties tool (e.g. setvia 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 theportal_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. viarolemap.xml):
roles = [r['name'] for r in portal.rolesOfPermission('My Permission') if r['selected']]
self.assertEqual(roles, ['Member', 'Manager'])
TraversalTo traverse to a view, page template or other resource, userestrictedTraverse() 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 mayraise Unauthorized if the current test user does not have permission toview 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 fromthat 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 intoaccount, and you cannot pass query string parameters. In fact,restrictedTraverse() and unrestrictedTraverse() implement the type oftraversal that happens with path expressions in TAL, which is similar, but notidentical 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 viewimplements 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 aresimulating a query string parameter or posted form variable that uses amarshaller like :int (e.g. age:int in the example above), the valuein 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 insteadof 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 requestis will not have the right URL or path information. If your view depends onthis, 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 interactionEnd-to-end functional tests can use zope.testbrowser to simulate userinteraction. This acts as a web browser, connecting to Zope via a specialchannel, making requests and obtaining responses.
[indent]Note: zope.testbrowser runs entirely in Python, and does not simulatea JavaScript engine.[/indent]Note that to use zope.testbrowser, you need to use one of the functionaltesting layers, e.g. PLONE_FUNCTIONAL_TESTING, or another layerinstantiated 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 lattercase, 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
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 formvariable name:
browser.getControl(name='age:int').value = u"30"
See the zope.testbrowser documentation for more details on how to selectand 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 variablename:
browser.getControl(name='form.button.Save').click()
To simulate HTTP BASIC authentication and remain logged in for allrequests:
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 tipsBy default, only HTTP error codes (e.g. 500 Server Side Error) are shown whenan error occurs on the server. To see more details, set handleErrors toFalse:
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 structureof the page. You should remove the file afterwards.