You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
96
52
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`.
97
71
```
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
98
77
install_requires=[
99
-
...
100
-
'plone.mocktestcase',
78
+
#...
79
+
"plone.mocktestcase",
101
80
],
102
81
```
103
82
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`.
106
84
107
-
```
85
+
```python
108
86
import unittest
109
87
110
-
...
88
+
#...
111
89
112
90
from plone.mocktestcase import MockTestCase
113
91
from zope.app.container.contained import ObjectAddedEvent
@@ -122,14 +100,14 @@ class TestPresenterUnit(MockTestCase):
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"`.
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.
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