Skip to content

Commit a5e04d5

Browse files
committed
Tidy testing-behaviors.md
1 parent 6ae26f6 commit a5e04d5

File tree

1 file changed

+144
-135
lines changed

1 file changed

+144
-135
lines changed
Lines changed: 144 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,158 +1,166 @@
11
---
22
myst:
33
html_meta:
4-
"description": ""
5-
"property=og:description": ""
6-
"property=og:title": ""
7-
"keywords": ""
4+
"description": "How to write unit tests for behaviors for content types in Plone"
5+
"property=og:description": "How to write unit tests for behaviors for content types in Plone"
6+
"property=og:title": "How to write unit tests for behaviors for content types in Plone"
7+
"keywords": "Plone, content types, testing, behaviors"
88
---
99

1010
# Testing behaviors
1111

12-
**How to write unit tests for behaviors**
12+
This chapter describes how to write unit tests for behaviors for content types in Plone.
1313

1414
Behaviors, like any other code, should be tested.
15-
If you are writing a behavior with just a marker interface or schema interface, it is probably not necessary to test the interface.
15+
If you write a behavior with just a marker interface or schema interface, it is probably not necessary to test the interface.
1616
However, any actual code, such as a behavior adapter factory, ought to be tested.
1717

1818
Writing a behavior integration test is not very difficult if you are happy to depend on Dexterity in your test.
19-
You can create a dummy type by instantiating a Dexterity FTI in *portal_types*.
20-
Then, enable your behavior by adding its interface name to the *behaviors* property.
19+
You can create a dummy type by instantiating a Dexterity FTI in `portal_types`.
20+
Then enable your behavior by adding its interface name to the `behaviors` property.
2121

2222
In many cases, however, it is better not to depend on Dexterity at all.
2323
It is not too difficult to mimic what Dexterity does to enable behaviors on its types.
24-
The following example is taken from *collective.gtags* and tests the *ITags* behavior we saw on the first page of this manual.
24+
The following example is taken from `collective.gtags` and tests the `ITags` behavior we saw on the first page of this manual.
2525

26-
```
27-
Behaviors
28-
=========
26+
27+
## Behaviors
2928

3029
This package provides a behavior called `collective.gtags.behaviors.ITags`.
31-
This adds a `Tags` field called `tags` to the "Categorization" fieldset, with
32-
a behavior adapter that stores the chosen tags in the Subject metadata field.
33-
34-
To learn more about the `Tags` field and how it works, see `tagging.rst`.
35-
36-
Test setup
37-
----------
38-
39-
Before we can run these tests, we need to load the collective.gtags
40-
configuration. This will configure the behavior.
41-
42-
>>> configuration = """\
43-
... <configure
44-
... xmlns="http://namespaces.zope.org/zope"
45-
... i18n_domain="collective.gtags">
46-
...
47-
... <include package="Products.Five" file="meta.zcml" />
48-
... <include package="collective.gtags" file="behaviors.zcml" />
49-
...
50-
... </configure>
51-
... """
52-
53-
>>> from StringIO import StringIO
54-
>>> from zope.configuration import xmlconfig
55-
>>> xmlconfig.xmlconfig(StringIO(configuration))
56-
57-
This behavior can be enabled for any `IDublinCore`. For the purposes of
58-
testing, we will use the CMFDefault Document type and a custom
59-
IBehaviorAssignable adapter to mark the behavior as enabled.
60-
61-
>>> from Products.CMFDefault.Document import Document
62-
63-
>>> from plone.behavior.interfaces import IBehaviorAssignable
64-
>>> from collective.gtags.behaviors import ITags
65-
>>> from zope.component import adapter
66-
>>> from zope.interface import implementer
67-
>>> @adapter(Document)
68-
... @implementer(IBehaviorAssignable)
69-
... class TestingAssignable(object):
70-
...
71-
... enabled = [ITags]
72-
...
73-
... def __init__(self, context):
74-
... self.context = context
75-
...
76-
... def supports(self, behavior_interface):
77-
... return behavior_interface in self.enabled
78-
...
79-
... def enumerate_behaviors(self):
80-
... for e in self.enabled:
81-
... yield queryUtility(IBehavior, name=e.__identifier__)
82-
83-
>>> from zope.component import provideAdapter
84-
>>> provideAdapter(TestingAssignable)
85-
86-
Behavior installation
87-
---------------------
88-
89-
We can now test that the behavior is installed when the ZCML for this package
90-
is loaded.
91-
92-
>>> from zope.component import getUtility
93-
>>> from plone.behavior.interfaces import IBehavior
94-
>>> tags_behavior = getUtility(IBehavior, name='collective.gtags.behaviors.ITags')
95-
>>> tags_behavior.interface
96-
<InterfaceClass collective.gtags.behaviors.ITags>
97-
98-
We also expect this behavior to be a form field provider. Let's verify that.
99-
100-
>>> from plone.autoform.interfaces import IFormFieldProvider
101-
>>> IFormFieldProvider.providedBy(tags_behavior.interface)
102-
True
30+
This adds a `Tags` field called `tags` to the `Categorization` fieldset, with a behavior adapter that stores the chosen tags in the `Subject` metadata field.
31+
32+
33+
### Test setup
34+
35+
Before we can run these tests, we need to load the `collective.gtags` configuration.
36+
This will configure the behavior.
37+
38+
```pycon
39+
>>> configuration = """\
40+
... <configure
41+
... xmlns="http://namespaces.zope.org/zope"
42+
... i18n_domain="collective.gtags">
43+
...
44+
... <include package="Products.Five" file="meta.zcml" />
45+
... <include package="collective.gtags" file="behaviors.zcml" />
46+
...
47+
... </configure>
48+
... """
49+
50+
>>> from StringIO import StringIO
51+
>>> from zope.configuration import xmlconfig
52+
>>> xmlconfig.xmlconfig(StringIO(configuration))
53+
```
54+
55+
This behavior can be enabled for any `IDublinCore`.
56+
For the purposes of testing, we will use the `CMFDefault` `Document` type and a custom `IBehaviorAssignable` adapter to mark the behavior as enabled.
57+
58+
```pycon
59+
>>> from Products.CMFDefault.Document import Document
60+
61+
>>> from plone.behavior.interfaces import IBehaviorAssignable
62+
>>> from collective.gtags.behaviors import ITags
63+
>>> from zope.component import adapter
64+
>>> from zope.interface import implementer
65+
>>> @adapter(Document)
66+
... @implementer(IBehaviorAssignable)
67+
... class TestingAssignable(object):
68+
...
69+
... enabled = [ITags]
70+
...
71+
... def __init__(self, context):
72+
... self.context = context
73+
...
74+
... def supports(self, behavior_interface):
75+
... return behavior_interface in self.enabled
76+
...
77+
... def enumerate_behaviors(self):
78+
... for e in self.enabled:
79+
... yield queryUtility(IBehavior, name=e.__identifier__)
80+
81+
>>> from zope.component import provideAdapter
82+
>>> provideAdapter(TestingAssignable)
83+
```
84+
85+
86+
### Behavior installation
87+
88+
We can now test that the behavior is installed when the ZCML for this package is loaded.
89+
90+
```pycon
91+
>>> from zope.component import getUtility
92+
>>> from plone.behavior.interfaces import IBehavior
93+
>>> tags_behavior = getUtility(IBehavior, name="collective.gtags.behaviors.ITags")
94+
>>> tags_behavior.interface
95+
<InterfaceClass collective.gtags.behaviors.ITags>
96+
```
97+
98+
We also expect this behavior to be a form field provider.
99+
Let's verify that.
100+
101+
```pycon
102+
>>> from plone.autoform.interfaces import IFormFieldProvider
103+
>>> IFormFieldProvider.providedBy(tags_behavior.interface)
104+
True
105+
```
106+
103107

104108
Using the behavior
105109
------------------
106110

107111
Let's create a content object that has this behavior enabled and check that
108112
it works.
109113

110-
>>> doc = Document('doc')
111-
>>> tags_adapter = ITags(doc, None)
112-
>>> tags_adapter is not None
113-
True
114+
```pycon
115+
>>> doc = Document("doc")
116+
>>> tags_adapter = ITags(doc, None)
117+
>>> tags_adapter is not None
118+
True
119+
```
114120

115121
We'll check that the `tags` set is built from the `Subject()` field:
116122

117-
>>> doc.setSubject(['One', 'Two'])
118-
>>> doc.Subject()
119-
('One', 'Two')
123+
```pycon
124+
>>> doc.setSubject(["One", "Two"])
125+
>>> doc.Subject()
126+
("One", "Two")
120127

121-
>>> tags_adapter.tags == set(['One', 'Two'])
122-
True
128+
>>> tags_adapter.tags == set(["One", "Two"])
129+
True
123130

124-
>>> tags_adapter.tags = set(['Two', 'Three'])
125-
>>> doc.Subject() == ('Two', 'Three')
126-
True
131+
>>> tags_adapter.tags = set(["Two", "Three"])
132+
>>> doc.Subject() == ("Two", "Three")
133+
True
127134
```
128135

129136
This test tries to prove that the behavior is correctly installed and works as intended on a suitable content class.
130137
It is not a true unit test, however.
131-
For a true unit test, we would simply test the *Tags* adapter directly on a dummy context, but that is not terribly interesting, since all it does is convert sets to tuples.
138+
For a true unit test, we would test the `Tags` adapter directly on a dummy context, but that is not terribly interesting, since all it does is convert sets to tuples.
132139

133140
First, we configure the package.
134-
To keep the test small, we limit ourselves to the *behaviors.zcml* file, which in this case will suffice.
135-
We still need to include a minimal set of ZCML from Five.
141+
To keep the test small, we limit ourselves to the {file}`behaviors.zcml` file, which in this case will suffice.
142+
We still need to include a minimal set of ZCML from `Five`.
136143

137-
Next, we implement an *IBehaviorAssignable\*adapter.
138-
This is a low-level component used by \*plone.behavior* to determine if a behavior is enabled on a particular object.
139-
Dexterity provides an implementation that checks the types FTI. Our test version is much simpler - it hardcodes the
140-
supported behaviors.
144+
Next, we implement an `IBehaviorAssignable` adapter.
145+
This is a low-level component used by `plone.behavior` to determine if a behavior is enabled on a particular object.
146+
Dexterity provides an implementation that checks the type's FTI.
147+
Our test version is much simpler: it hardcodes the supported behaviors.
141148

142-
With this in place, we first check that the *IBehavior* utility has been correctly registered.
143-
This is essentially a test to show that weve used the *\<plone:behavior />* directive as intended.
144-
We also verify that our schema interface is an *IFormFieldsProvider*.
145-
For a non-form behavior, wed omit this.
149+
With this in place, we first check that the `IBehavior` utility has been correctly registered.
150+
This is essentially a test to show that we've used the `<plone:behavior />` directive as intended.
151+
We also verify that our schema interface is an `IFormFieldsProvider`.
152+
For a non-form behavior, we'd omit this.
146153

147154
Finally, we test the behavior.
148-
Weve chosen to use CMFDefault’s *Document* type for our test, as the behavior adapter requires an object providing *IDublinCore*.
149-
Ideally, wed write our own class and implement *IDublinCore* directly.
150-
However, in many cases, the types from CMFDefault are going to provide convenient test fodder.
155+
We've chosen to use CMFDefault's `Document` type for our test, as the behavior adapter requires an object providing `IDublinCore`.
156+
Ideally, we'd write our own class and implement `IDublinCore` directly.
157+
However, in many cases, the types from `CMFDefault` are going to provide convenient test fodder.
151158

152-
If our behavior was more complex wed add more intricate tests.
159+
If our behavior was more complex, we'd add more intricate tests.
153160
By the last section of the doctest, we have enough context to test the adapter factory.
154161

155-
To run the test, we need a test suite. In *tests.py*, we have:
162+
To run the test, we need a test suite.
163+
Here is our {file}`tests.py`.
156164

157165
```python
158166
from zope.app.testing import setup
@@ -168,28 +176,28 @@ def tearDown(test):
168176
def test_suite():
169177
return unittest.TestSuite((
170178
doctest.DocFileSuite(
171-
'behaviors.rst',
179+
"behaviors.rst",
172180
setUp=setUp, tearDown=tearDown,
173181
optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS),
174182
))
175183
```
176184

177-
This runs the *behaviors.rst* doctest from the same directory as the *tests.py* file.
178-
To run the test, we can use the usual test runner:
185+
This runs the {file}`behaviors.rst` doctest from the same directory as the {file}`tests.py` file.
186+
To run the test, we can use the usual test runner.
179187

180-
```
181-
$ ./bin/instance test -s collective.gtags
188+
```shell
189+
./bin/instance test -s collective.gtags
182190
```
183191

184-
## Testing a dexterity type with a behavior
192+
193+
## Testing a Dexterity type with a behavior
185194

186195
Not all behaviors are enabled by default.
187196
Let's say you want to test your Dexterity type when a behavior is enabled or disabled.
188197
To do this, you will need to setup the behavior in your test.
189-
There is an example of this kind of test in the collective.cover product.
198+
There is an example of this kind of test in the `collective.cover` product.
190199
There is a behavior that adds the capability for the cover page to refresh itself.
191-
The test checks if the behavior is not yet enabled, enables the behavior, check its effect and then disables it again.
192-
Here is the code:
200+
The test checks if the behavior is not yet enabled, enables the behavior, check its effect, and then disables it again.
193201

194202
```python
195203
# -*- coding: utf-8 -*-
@@ -212,43 +220,43 @@ class RefreshBehaviorTestCase(unittest.TestCase):
212220
layer = INTEGRATION_TESTING
213221

214222
def _enable_refresh_behavior(self):
215-
fti = queryUtility(IDexterityFTI, name='collective.cover.content')
223+
fti = queryUtility(IDexterityFTI, name="collective.cover.content")
216224
behaviors = list(fti.behaviors)
217225
behaviors.append(IRefresh.__identifier__)
218226
fti.behaviors = tuple(behaviors)
219227
# invalidate schema cache
220-
notify(SchemaInvalidatedEvent('collective.cover.content'))
228+
notify(SchemaInvalidatedEvent("collective.cover.content"))
221229

222230
def _disable_refresh_behavior(self):
223-
fti = queryUtility(IDexterityFTI, name='collective.cover.content')
231+
fti = queryUtility(IDexterityFTI, name="collective.cover.content")
224232
behaviors = list(fti.behaviors)
225233
behaviors.remove(IRefresh.__identifier__)
226234
fti.behaviors = tuple(behaviors)
227235
# invalidate schema cache
228-
notify(SchemaInvalidatedEvent('collective.cover.content'))
236+
notify(SchemaInvalidatedEvent("collective.cover.content"))
229237

230238
def setUp(self):
231-
self.portal = self.layer['portal']
232-
self.request = self.layer['request']
239+
self.portal = self.layer["portal"]
240+
self.request = self.layer["request"]
233241
alsoProvides(self.request, ICoverLayer)
234-
with api.env.adopt_roles(['Manager']):
242+
with api.env.adopt_roles(["Manager"]):
235243
self.cover = api.content.create(
236-
self.portal, 'collective.cover.content', 'c1')
244+
self.portal, "collective.cover.content", "c1")
237245

238246
def test_refresh_registration(self):
239247
registration = queryUtility(IBehavior, name=IRefresh.__identifier__)
240248
self.assertIsNotNone(registration)
241249

242250
def test_refresh_behavior(self):
243-
view = api.content.get_view(u'view', self.cover, self.request)
244-
self.assertNotIn('<meta http-equiv="refresh" content="300" />', view())
251+
view = api.content.get_view(u"view", self.cover, self.request)
252+
self.assertNotIn("<meta http-equiv="refresh" content="300" />", view())
245253
self._enable_refresh_behavior()
246254
self.cover.enable_refresh = True
247-
self.assertIn('<meta http-equiv="refresh" content="300" />', view())
255+
self.assertIn("<meta http-equiv="refresh" content="300" />", view())
248256
self.cover.ttl = 5
249-
self.assertIn('<meta http-equiv="refresh" content="5" />', view())
257+
self.assertIn("<meta http-equiv="refresh" content="5" />", view())
250258
self._disable_refresh_behavior()
251-
self.assertNotIn('<meta http-equiv="refresh" content="5" />', view())
259+
self.assertNotIn("<meta http-equiv="refresh" content="5" />", view())
252260
```
253261

254262
The methods `_enable_refresh_behavior` and `_disable_refresh_behavior` use the `IDexterityFTI` to get the Factory Type Information for the Dexterity type (`collective.cover.content` in this case).
@@ -258,7 +266,8 @@ To disable it, remove the behavior from the FTI behaviors: `behaviors.remove(IRe
258266
Assign the resulting behaviors list to the behaviors attribute of the FTI as a tuple: `fti.behaviors = tuple(behaviors)`.
259267
Finally, to have the changes take effect, invalidate the schema cache: `notify(SchemaInvalidatedEvent('collective.cover.content'))`.
260268

269+
261270
## A note about marker interfaces
262271

263272
Marker interface support depends on code that is implemented in Dexterity and is non-trivial to reproduce in a test.
264-
If you need a marker interface in a test, set it manually with *zope.interface.alsoProvides*, or write an integration test with Dexterity content.
273+
If you need a marker interface in a test, set it manually with `zope.interface.alsoProvides`, or write an integration test with Dexterity content.

0 commit comments

Comments
 (0)