Skip to content

Commit 8c9e9a4

Browse files
committed
Tidy mock-testing.md
1 parent a85c02d commit 8c9e9a4

File tree

1 file changed

+109
-168
lines changed

1 file changed

+109
-168
lines changed
Lines changed: 109 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,29 @@
11
---
22
myst:
33
html_meta:
4-
"description": ""
5-
"property=og:description": ""
6-
"property=og:title": ""
7-
"keywords": ""
4+
"description": "How to use a mock objects framework to write mock based tests of content types in Plone"
5+
"property=og:description": "How to use a mock objects framework to write mock based tests of content types in Plone"
6+
"property=og:title": "How to use a mock objects framework to write mock based tests of content types in Plone"
7+
"keywords": "Plone, content types, tests, mock objects, framework"
88
---
99

1010
# Mock testing
1111

12-
**Using a mock objects framework to write mock based tests**
12+
This chapter describes how to use a mock objects framework to write mock based tests.
1313

14-
Mock testing is a powerful approach to testing that lets you make
15-
assertions about how the code under test is interacting with other
16-
system modules. It is often useful when the code you want to test is
17-
performing operations that cannot be easily asserted by looking at its
18-
return value.
14+
Mock testing is a powerful approach to testing that lets you make assertions about how the code under test is interacting with other system modules.
15+
It is often useful when the code you want to test is performing operations that cannot be easily asserted by looking at its return value.
1916

20-
In our example product, we have an event handler like this:
17+
In our example product, we have an event handler.
2118

22-
```
19+
```python
2320
def notifyUser(presenter, event):
24-
acl_users = getToolByName(presenter, 'acl_users')
25-
mail_host = getToolByName(presenter, 'MailHost')
26-
portal_url = getToolByName(presenter, 'portal_url')
21+
acl_users = getToolByName(presenter, "acl_users")
22+
mail_host = getToolByName(presenter, "MailHost")
23+
portal_url = getToolByName(presenter, "portal_url")
2724

2825
portal = portal_url.getPortalObject()
29-
sender = portal.getProperty('email_from_address')
26+
sender = portal.getProperty("email_from_address")
3027

3128
if not sender:
3229
return
@@ -36,78 +33,59 @@ def notifyUser(presenter, event):
3633

3734
matching_users = acl_users.searchUsers(fullname=presenter.title)
3835
for user_info in matching_users:
39-
email = user_info.get('email', None)
36+
email = user_info.get("email", None)
4037
if email is not None:
4138
mail_host.send(message, email, sender, subject)
4239
```
4340

44-
If we want to test that this sends the right kind of email message,
45-
we’ll need to somehow inspect what is passed to *send()*. The only
46-
way to do that is to replace the *MailHost\*object that is acquired when
47-
\*getToolByName(presenter, ‘MailHost’)* is called, with something that
48-
performs that assertion for us.
49-
50-
If we wanted to write an integration test, we could use *PloneTestCase*
51-
to execute this event handler, e.g. by firing the event manually, and
52-
temporarily replace the *MailHost* object in the root of the test case
53-
portal (*self.portal*) with a dummy that raised an exception if the
54-
wrong value was passed.
55-
56-
However, such integration tests can get pretty heavy handed, and
57-
sometimes it is difficult to ensure that it works in all cases. In the
58-
approach outlined above, for example, we would miss cases where no mail
59-
was sent at all.
60-
61-
Enter mock objects. A mock object is a “test double” that knows how and
62-
when it ought to be called. The typical approach is as follows:
63-
64-
- Create a mock object.
65-
- The mock object starts out in “record” mode.
66-
- Record the operations that you expect the code under test perform on
67-
the mock object. You can make assertions about the type and value of
68-
arguments, the sequence of calls, or the number of times a method is
69-
called or an attribute is retrieved or set.
70-
- You can also give your mock objects behaviour, e.g. by specifying
71-
return values or exceptions to be raised in certain cases.
72-
- Initialise the code under test and/or the environment it runs in so
73-
that it will use the mock object rather than the real object.
74-
Sometimes this involves temporarily “patching” the environment.
75-
- Put the mock framework into “replay” mode.
76-
- Run the code under test.
77-
- Apply any assertions as you normally would.
78-
- The mock framework will raise exceptions if the mock objects are
79-
called incorrectly (e.g. with the wrong arguments, or too many times)
80-
or insufficiently (e.g. an expected method was not called).
81-
82-
There are several Python mock object frameworks. Dexterity itself users
83-
a powerful one called [mocker], via the [plone.mocktestcase]
84-
integration package. You are encouraged to read the documentation for
85-
those two packages to better understand how mock testing works, and what
86-
options are available.
87-
88-
:::{note}
89-
Take a look at the tests in *plone.dexterity* if you’re looking for more
90-
examples of mock tests using *plone.mocktestcase*.
91-
:::
92-
93-
To use the mock testing framework, we first need to depend on
94-
*plone.mocktestcase*. As usual, we add it to *setup.py* and re-run
95-
buildout.
41+
If we want to test that this sends the right kind of email message, we'll need to somehow inspect what is passed to `send().`
42+
The only way to do that is to replace the `MailHost` object that is acquired when `getToolByName(presenter, ‘MailHost')` is called, with something that performs that assertion for us.
43+
44+
If we wanted to write an integration test, we could use `PloneTestCase` to execute this event handler by firing the event manually, and temporarily replace the `MailHost` object in the root of the test case portal (`self.portal`) with a dummy that raised an exception if the wrong value was passed.
45+
46+
However, such integration tests can get pretty heavy handed, and sometimes it is difficult to ensure that it works in all cases.
47+
In the approach outlined above, for example, we would miss cases where no mail was sent at all.
48+
49+
Enter mock objects.
50+
A mock object is a "test double" that knows how and when it ought to be called.
51+
The typical approach is as follows.
9652

53+
- Create a mock object.
54+
- The mock object starts out in "record" mode.
55+
- Record the operations that you expect the code under test perform on the mock object.
56+
You can make assertions about the type and value of arguments, the sequence of calls, the number of times a method is called, or whether an attribute is retrieved or set.
57+
- You can also give your mock objects behavior, such as specifying return values or exceptions to be raised in certain cases.
58+
- Initialize the code under test or the environment it runs in so that it will use the mock object rather than the real object.
59+
Sometimes this involves temporarily "patching" the environment.
60+
- Put the mock framework into "replay" mode.
61+
- Run the code under test.
62+
- Apply any assertions as you normally would.
63+
- The mock framework will raise exceptions if the mock objects are called incorrectly, such as with the wrong arguments or too many times, or insufficiently, such as an expected method was not called.
64+
65+
There are several Python mock object frameworks.
66+
Dexterity itself uses a powerful one called [`mocker`](https://labix.org/mocker), via the [`plone.mocktestcase`](https://pypi.org/project/plone.mocktestcase/) integration package.
67+
You are encouraged to read the documentation for those two packages to better understand how mock testing works, and what options are available.
68+
69+
```{note}
70+
Take a look at the tests in `plone.dexterity` if you're looking for more examples of mock tests using `plone.mocktestcase`.
9771
```
72+
73+
To use the mock testing framework, we first need to depend on `plone.mocktestcase`.
74+
As usual, we add it to {file}`setup.py` and re-run buildout.
75+
76+
```python
9877
install_requires=[
99-
...
100-
'plone.mocktestcase',
78+
# ...
79+
"plone.mocktestcase",
10180
],
10281
```
10382

104-
As an example test case, consider the following class in
105-
*test_presenter.py*:
83+
As an example test case, consider the following class in {file}`test_presenter.py`.
10684

107-
```
85+
```python
10886
import unittest
10987

110-
...
88+
# ...
11189

11290
from plone.mocktestcase import MockTestCase
11391
from zope.app.container.contained import ObjectAddedEvent
@@ -122,14 +100,14 @@ class TestPresenterUnit(MockTestCase):
122100
__parent__=None,
123101
__name__=None,
124102
title="Jim",
125-
absolute_url = lambda: 'http://example.org/presenter',
103+
absolute_url = lambda: "http://example.org/presenter",
126104
)
127105

128106
# dummy event
129107
event = ObjectAddedEvent(presenter)
130108

131109
# search result for acl_users
132-
user_info = [{'email': '[email protected]', 'id': 'jim'}]
110+
user_info = [{"email": "[email protected]", "id": "jim"}]
133111

134112
# email data
135113
message = "A presenter called Jim was added here http://example.org/presenter"
@@ -140,18 +118,18 @@ class TestPresenterUnit(MockTestCase):
140118
# mock tools/portal
141119

142120
portal_mock = self.mocker.mock()
143-
self.expect(portal_mock.getProperty('email_from_address')).result('[email protected]')
121+
self.expect(portal_mock.getProperty("email_from_address")).result("[email protected]")
144122

145123
portal_url_mock = self.mocker.mock()
146-
self.mock_tool(portal_url_mock, 'portal_url')
124+
self.mock_tool(portal_url_mock, "portal_url")
147125
self.expect(portal_url_mock.getPortalObject()).result(portal_mock)
148126

149127
acl_users_mock = self.mocker.mock()
150-
self.mock_tool(acl_users_mock, 'acl_users')
151-
self.expect(acl_users_mock.searchUsers(fullname='Jim')).result(user_info)
128+
self.mock_tool(acl_users_mock, "acl_users")
129+
self.expect(acl_users_mock.searchUsers(fullname="Jim")).result(user_info)
152130

153131
mail_host_mock = self.mocker.mock()
154-
self.mock_tool(mail_host_mock, 'MailHost')
132+
self.mock_tool(mail_host_mock, "MailHost")
155133
self.expect(mail_host_mock.send(message, email, sender, subject))
156134

157135

@@ -165,99 +143,62 @@ class TestPresenterUnit(MockTestCase):
165143
# returned something. The mock framework will verify the assertions
166144
# about expected call sequences.
167145

168-
...
146+
# ...
169147

170148
def test_suite():
171149
return unittest.defaultTestLoader.loadTestsFromName(__name__)
172150
```
173151

174-
Note that the other tests in this module have been removed for the sake
175-
of brevity.
176-
177-
If you are not familiar with mock testing, it may take a bit of time to
178-
get your head around what’s going on here. Let’s run though the test:
179-
180-
- First, we create a dummy presenter object. This is *not* a mock
181-
object, it’s just a class with the required minimum set of
182-
attributes, created using the *create_dummy()* helper method from
183-
the *MockTestCase* base class. We use this type of dummy because we
184-
are not interested in making any assertions on the *presenter*
185-
object: it is used as an “input” only.
186-
- Next, we create a dummy event. Here we have opted to use a standard
187-
implementation from *zope.app.container*.
188-
- We then define a few variables that we will use in the various
189-
assertions and mock return values: the user data that will form our
190-
dummy user search results, and the email data passed to the mail
191-
host.
192-
- Next, we create mocks for each of the tools that our code needs to
193-
look up. For each, we use the *expect()* method from *MockTestCase*
194-
to make some assertions. For example, we expect that
195-
*getPortalObject()* will be called (once) on the *portal_url* tool,
196-
and it should return another mock object, the *portal_mock*. On
197-
this, we expect that *getProperty()* is called with an argument equal
198-
to *“email_from_address”*. The mock will then return
199-
*[email protected]*. Take a look at the *mocker* and
200-
*plone.mocktestcase* documentation to see the various other types of
201-
assertions you can make.
202-
- The most important mock assertion is the line
203-
*self.expect(mail_host_mock.send(message, email, sender,
204-
subject))*. This asserts that the *send()* method gets called
205-
with the required message, recipient address, sender address and
206-
subject, exactly once.
207-
- We then put the mock into replay mode, using *self.replay()*. Up
208-
until this point, any calls on our mock objects have been to record
209-
expectations and specify behaviour. From now on, any call will count
210-
towards verifying those expectations.
211-
- Finally, we call the code under test with our dummy presenter and
212-
event.
213-
- In this case, we don’t have any “normal” assertions, although the
214-
usual unit test assertion methods are all available if you need them,
215-
e.g. to test the return value of the method under test. The
216-
assertions in this case are all coming from the mock objects. The
217-
*tearDown()* method of the *MockTestCase* class will in fact check
218-
that all the various methods were called exactly as expected.
219-
220-
To run these tests, use the normal test runner, e.g.:
221-
222-
```
223-
$ ./bin/test example.conference -t TestPresenterMock
152+
Note that the other tests in this module have been removed for the sake of brevity.
153+
154+
If you are not familiar with mock testing, it may take a bit of time to get your head around what's going on here.
155+
Let's run though the test.
156+
157+
- First, we create a dummy presenter object.
158+
This is *not* a mock object, it's just a class with the required minimum set of attributes, created using the `create_dummy()` helper method from the `MockTestCase` base class.
159+
We use this type of dummy because we are not interested in making any assertions on the `presenter` object: it is used as an "input" only.
160+
- Next, we create a dummy event.
161+
Here we have opted to use a standard implementation from `zope.app.container`.
162+
- We then define a few variables that we will use in the various assertions and mock return values: the user data that will form our dummy user search results, and the email data passed to the mail host.
163+
- Next, we create mocks for each of the tools that our code needs to look up.
164+
For each, we use the `expect()` method from `MockTestCase` to make some assertions.
165+
For example, we expect that `getPortalObject()` will be called (once) on the `portal_url` tool, and it should return another mock object, the `portal_mock`.
166+
On this, we expect that `getProperty()` is called with an argument equal to `"email_from_address"`.
167+
The mock will then return `"[email protected]"`.
168+
Take a look at the `mocker` and `plone.mocktestcase` documentation to see the various other types of assertions you can make.
169+
- The most important mock assertion is the line `self.expect(mail_host_mock.send(message, email, sender, subject))`.
170+
This asserts that the `send()` method gets called with the required message, recipient address, sender address, and subject, exactly once.
171+
- We then put the mock into replay mode, using `self.replay()`.
172+
Up until this point, any calls on our mock objects have been to record expectations and specify behaviour.
173+
From now on, any call will count towards verifying those expectations.
174+
- Finally, we call the code under test with our dummy presenter and event.
175+
- In this case, we don't have any "normal" assertions, although the usual unit test assertion methods are all available if you need them, for example, to test the return value of the method under test.
176+
The assertions in this case are all coming from the mock objects.
177+
The `tearDown()` method of the `MockTestCase` class will in fact check that all the various methods were called exactly as expected.
178+
179+
To run these tests, use the normal test runner.
180+
181+
```shell
182+
./bin/test example.conference -t TestPresenterMock
224183
```
225184

226-
Note that mock tests are typically as fast as unit tests, so there is
227-
typically no need for something like roadrunner.
185+
Note that mock tests are typically as fast as unit tests, so there is typically no need for something like roadrunner.
186+
228187

229188
## Mock testing caveats
230189

231-
Mock testing is a somewhat controversial topic. On the one hand, it
232-
allows you to write tests for things that are often difficult to test,
233-
and a mock framework can - once you are familiar with it - make child’s
234-
play out of the often laborious task of creating reliable test doubles.
235-
On the other hand, mock based tests are inevitably tied to the
236-
implementation of the code under test, and sometimes this coupling can
237-
be too tight for the test to be meaningful. Using mock objects normally
238-
also means that you need a very good understanding of the external APIs
239-
you are mocking. Otherwise, your mock may not be a good representation
240-
of how these systems would behave in the real world. Much has been
241-
written on this, for example by [Martin Fowler].
242-
243-
As always, it pays to be pragmatic. If you find that you can’t write a
244-
mock based test without reading every line of code in the method under
245-
test and reverse engineering it for the mocks, then an integration test
246-
may be more appropriate. In fact, it is prudent to have at least some
247-
integration tests in any case, since you can never be 100% sure your
248-
mocks are valid representations of the real objects they are mocking.
249-
250-
On the other hand, if the code you are testing is using well-defined
251-
APIs in a relatively predictable manner, mock objects can be a valuable
252-
way to test the “side effects” of your code, and a helpful tool to
253-
simulate things like exceptions and input values that may be difficult
254-
to produce otherwise.
255-
256-
Remember also that mock objects are not necessarily an “all or nothing”
257-
proposition. You can use simple dummy objects or “real” instances in
258-
most cases, and augment them with a few mock objects for those
259-
difficult-to-replicate test cases.
260-
261-
[martin fowler]: http://www.martinfowler.com/articles/mocksArentStubs.html
262-
[mocker]: http://labix.org/mocker
263-
[plone.mocktestcase]: http://pypi.python.org/pypi/plone.mocktestcase
190+
Mock testing is a somewhat controversial topic.
191+
On the one hand, it allows you to write tests for things that are often difficult to test, and a mock framework can—once you are familiar with it—make child's play out of the often laborious task of creating reliable test doubles.
192+
On the other hand, mock based tests are inevitably tied to the implementation of the code under test, and sometimes this coupling can be too tight for the test to be meaningful.
193+
Using mock objects normally also means that you need a very good understanding of the external APIs you are mocking.
194+
Otherwise, your mock may not be a good representation of how these systems would behave in the real world.
195+
Much has been written on this, including [_Mocks Aren't Stubs_ by Martin Fowler](https://www.martinfowler.com/articles/mocksArentStubs.html).
196+
197+
As always, it pays to be pragmatic.
198+
If you find that you can't write a mock based test without reading every line of code in the method under test and reverse engineering it for the mocks, then an integration test may be more appropriate.
199+
In fact, it is prudent to have at least some integration tests in any case, since you can never be 100% sure your mocks are valid representations of the real objects they are mocking.
200+
201+
On the other hand, if the code you are testing is using well-defined APIs in a relatively predictable manner, mock objects can be a valuable way to test the "side effects" of your code, and a helpful tool to simulate things like exceptions and input values that may be difficult to produce otherwise.
202+
203+
Remember also that mock objects are not necessarily an "all or nothing" proposition.
204+
You can use simple dummy objects or "real" instances in most cases, and augment them with a few mock objects for those difficult-to-replicate test cases.

0 commit comments

Comments
 (0)