@@ -8,46 +8,215 @@ Sometimes tests need to invoke functionality which depends
8
8
on global settings or which invokes code which cannot be easily
9
9
tested such as network access. The ``monkeypatch `` fixture
10
10
helps you to safely set/delete an attribute, dictionary item or
11
- environment variable or to modify ``sys.path `` for importing.
11
+ environment variable, or to modify ``sys.path `` for importing.
12
+
13
+ The ``monkeypatch `` fixture provides these helper methods for safely patching and mocking
14
+ functionality in tests:
15
+
16
+ .. code-block :: python
17
+
18
+ monkeypatch.setattr(obj, name, value, raising = True )
19
+ monkeypatch.delattr(obj, name, raising = True )
20
+ monkeypatch.setitem(mapping, name, value)
21
+ monkeypatch.delitem(obj, name, raising = True )
22
+ monkeypatch.setenv(name, value, prepend = False )
23
+ monkeypatch.delenv(name, raising = True )
24
+ monkeypatch.syspath_prepend(path)
25
+ monkeypatch.chdir(path)
26
+
27
+ All modifications will be undone after the requesting
28
+ test function or fixture has finished. The ``raising ``
29
+ parameter determines if a ``KeyError `` or ``AttributeError ``
30
+ will be raised if the target of the set/deletion operation does not exist.
31
+
32
+ Consider the following scenarios:
33
+
34
+ 1. Modifying the behavior of a function or the property of a class for a test e.g.
35
+ there is an API call or database connection you will not make for a test but you know
36
+ what the expected output should be. Use :py:meth: `monkeypatch.setattr ` to patch the
37
+ function or property with your desired testing behavior. This can include your own functions.
38
+ Use :py:meth: `monkeypatch.delattr ` to remove the function or property for the test.
39
+
40
+ 2. Modifying the values of dictionaries e.g. you have a global configuration that
41
+ you want to modify for certain test cases. Use :py:meth: `monkeypatch.setitem ` to patch the
42
+ dictionary for the test. :py:meth: `monkeypatch.delitem ` can be used to remove items.
43
+
44
+ 3. Modifying environment variables for a test e.g. to test program behavior if an
45
+ environment variable is missing, or to set multiple values to a known variable.
46
+ :py:meth: `monkeypatch.setenv ` and :py:meth: `monkeypatch.delenv ` can be used for
47
+ these patches.
48
+
49
+ 4. Use :py:meth: `monkeypatch.syspath_prepend ` to modify the system ``$PATH `` safely, and
50
+ :py:meth: `monkeypatch.chdir ` to change the context of the current working directory
51
+ during a test.
52
+
12
53
See the `monkeypatch blog post `_ for some introduction material
13
54
and a discussion of its motivation.
14
55
15
56
.. _`monkeypatch blog post` : http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/
16
57
17
-
18
58
Simple example: monkeypatching functions
19
59
----------------------------------------
20
60
21
- If you want to pretend that ``os.expanduser `` returns a certain
22
- directory, you can use the :py:meth: `monkeypatch.setattr ` method to
23
- patch this function before calling into a function which uses it::
61
+ Consider a scenario where you are working with user directories. In the context of
62
+ testing, you do not want your test to depend on the running user. ``monkeypatch ``
63
+ can be used to patch functions dependent on the user to always return a
64
+ specific value.
65
+
66
+ In this example, :py:meth: `monkeypatch.setattr ` is used to patch ``Path.home ``
67
+ so that the known testing path ``Path("/abc") `` is always used when the test is run.
68
+ This removes any dependency on the running user for testing purposes.
69
+ :py:meth: `monkeypatch.setattr ` must be called before the function which will use
70
+ the patched function is called.
71
+ After the test function finishes the ``Path.home `` modification will be undone.
72
+
73
+ .. code-block :: python
74
+
75
+ # contents of test_module.py with source code and the test
76
+ from pathlib import Path
77
+
78
+
79
+ def getssh ():
80
+ """ Simple function to return expanded homedir ssh path."""
81
+ return Path.home() / " .ssh"
82
+
83
+
84
+ def test_getssh (monkeypatch ):
85
+ # mocked return function to replace Path.home
86
+ # always return '/abc'
87
+ def mockreturn ():
88
+ return Path(" /abc" )
24
89
25
- # content of test_module.py
26
- import os.path
27
- def getssh(): # pseudo application code
28
- return os.path.join(os.path.expanduser("~admin"), '.ssh')
90
+ # Application of the monkeypatch to replace Path.home
91
+ # with the behavior of mockreturn defined above.
92
+ monkeypatch.setattr(Path, " home" , mockreturn)
29
93
30
- def test_mytest(monkeypatch):
31
- def mockreturn(path):
32
- return '/abc'
33
- monkeypatch.setattr(os.path, 'expanduser', mockreturn)
94
+ # Calling getssh() will use mockreturn in place of Path.home
95
+ # for this test with the monkeypatch.
34
96
x = getssh()
35
- assert x == '/abc/.ssh'
97
+ assert x == Path(" /abc/.ssh" )
98
+
99
+ Monkeypatching returned objects: building mock classes
100
+ ------------------------------------------------------
101
+
102
+ :py:meth: `monkeypatch.setattr ` can be used in conjunction with classes to mock returned
103
+ objects from functions instead of values.
104
+ Imagine a simple function to take an API url and return the json response.
105
+
106
+ .. code-block :: python
107
+
108
+ # contents of app.py, a simple API retrieval example
109
+ import requests
110
+
111
+
112
+ def get_json (url ):
113
+ """ Takes a URL, and returns the JSON."""
114
+ r = requests.get(url)
115
+ return r.json()
116
+
117
+ We need to mock ``r ``, the returned response object for testing purposes.
118
+ The mock of ``r `` needs a ``.json() `` method which returns a dictionary.
119
+ This can be done in our test file by defining a class to represent ``r ``.
120
+
121
+ .. code-block :: python
122
+
123
+ # contents of test_app.py, a simple test for our API retrieval
124
+ # import requests for the purposes of monkeypatching
125
+ import requests
126
+
127
+ # our app.py that includes the get_json() function
128
+ # this is the previous code block example
129
+ import app
130
+
131
+ # custom class to be the mock return value
132
+ # will override the requests.Response returned from requests.get
133
+ class MockResponse :
134
+
135
+ # mock json() method always returns a specific testing dictionary
136
+ @ staticmethod
137
+ def json ():
138
+ return {" mock_key" : " mock_response" }
139
+
140
+
141
+ def test_get_json (monkeypatch ):
142
+
143
+ # Any arguments may be passed and mock_get() will always return our
144
+ # mocked object, which only has the .json() method.
145
+ def mock_get (* args , ** kwargs ):
146
+ return MockResponse()
147
+
148
+ # apply the monkeypatch for requests.get to mock_get
149
+ monkeypatch.setattr(requests, " get" , mock_get)
150
+
151
+ # app.get_json, which contains requests.get, uses the monkeypatch
152
+ result = app.get_json(" https://fakeurl" )
153
+ assert result[" mock_key" ] == " mock_response"
154
+
155
+
156
+ ``monkeypatch `` applies the mock for ``requests.get `` with our ``mock_get `` function.
157
+ The ``mock_get `` function returns an instance of the ``MockResponse `` class, which
158
+ has a ``json() `` method defined to return a known testing dictionary and does not
159
+ require any outside API connection.
160
+
161
+ You can build the ``MockResponse `` class with the appropriate degree of complexity for
162
+ the scenario you are testing. For instance, it could include an ``ok `` property that
163
+ always returns ``True ``, or return different values from the ``json() `` mocked method
164
+ based on input strings.
165
+
166
+ This mock can be shared across tests using a ``fixture ``:
167
+
168
+ .. code-block :: python
169
+
170
+ # contents of test_app.py, a simple test for our API retrieval
171
+ import pytest
172
+ import requests
173
+
174
+ # app.py that includes the get_json() function
175
+ import app
176
+
177
+ # custom class to be the mock return value of requests.get()
178
+ class MockResponse :
179
+ @ staticmethod
180
+ def json ():
181
+ return {" mock_key" : " mock_response" }
182
+
183
+
184
+ # monkeypatched requests.get moved to a fixture
185
+ @pytest.fixture
186
+ def mock_response (monkeypatch ):
187
+ """ Requests.get() mocked to return {'mock_key':'mock_response'}."""
188
+
189
+ def mock_get (* args , ** kwargs ):
190
+ return MockResponse()
191
+
192
+ monkeypatch.setattr(requests, " get" , mock_get)
193
+
194
+
195
+ # notice our test uses the custom fixture instead of monkeypatch directly
196
+ def test_get_json (mock_response ):
197
+ result = app.get_json(" https://fakeurl" )
198
+ assert result[" mock_key" ] == " mock_response"
199
+
200
+
201
+ Furthermore, if the mock was designed to be applied to all tests, the ``fixture `` could
202
+ be moved to a ``conftest.py `` file and use the with ``autouse=True `` option.
36
203
37
- Here our test function monkeypatches ``os.path.expanduser `` and
38
- then calls into a function that calls it. After the test function
39
- finishes the ``os.path.expanduser `` modification will be undone.
40
204
41
205
Global patch example: preventing "requests" from remote operations
42
206
------------------------------------------------------------------
43
207
44
208
If you want to prevent the "requests" library from performing http
45
- requests in all your tests, you can do::
209
+ requests in all your tests, you can do:
46
210
47
- # content of conftest.py
211
+ .. code-block :: python
212
+
213
+ # contents of conftest.py
48
214
import pytest
215
+
216
+
49
217
@pytest.fixture (autouse = True )
50
218
def no_requests (monkeypatch ):
219
+ """ Remove requests.sessions.Session.request for all tests."""
51
220
monkeypatch.delattr(" requests.sessions.Session.request" )
52
221
53
222
This autouse fixture will be executed for each test function and it
@@ -85,7 +254,7 @@ Monkeypatching environment variables
85
254
------------------------------------
86
255
87
256
If you are working with environment variables you often need to safely change the values
88
- or delete them from the system for testing purposes. ``Monkeypatch `` provides a mechanism
257
+ or delete them from the system for testing purposes. ``monkeypatch `` provides a mechanism
89
258
to do this using the ``setenv `` and ``delenv `` method. Our example code to test:
90
259
91
260
.. code-block :: python
@@ -131,6 +300,7 @@ This behavior can be moved into ``fixture`` structures and shared across tests:
131
300
132
301
.. code-block :: python
133
302
303
+ # contents of our test file e.g. test_code.py
134
304
import pytest
135
305
136
306
@@ -144,7 +314,7 @@ This behavior can be moved into ``fixture`` structures and shared across tests:
144
314
monkeypatch.delenv(" USER" , raising = False )
145
315
146
316
147
- # Notice the tests reference the fixtures for mocks
317
+ # notice the tests reference the fixtures for mocks
148
318
def test_upper_to_lower (mock_env_user ):
149
319
assert get_os_user_lower() == " testinguser"
150
320
@@ -154,6 +324,112 @@ This behavior can be moved into ``fixture`` structures and shared across tests:
154
324
_ = get_os_user_lower()
155
325
156
326
327
+ Monkeypatching dictionaries
328
+ ---------------------------
329
+
330
+ :py:meth: `monkeypatch.setitem ` can be used to safely set the values of dictionaries
331
+ to specific values during tests. Take this simplified connection string example:
332
+
333
+ .. code-block :: python
334
+
335
+ # contents of app.py to generate a simple connection string
336
+ DEFAULT_CONFIG = {" user" : " user1" , " database" : " db1" }
337
+
338
+
339
+ def create_connection_string (config = None ):
340
+ """ Creates a connection string from input or defaults."""
341
+ config = config or DEFAULT_CONFIG
342
+ return f " User Id= { config[' user' ]} ; Location= { config[' database' ]} ; "
343
+
344
+ For testing purposes we can patch the ``DEFAULT_CONFIG `` dictionary to specific values.
345
+
346
+ .. code-block :: python
347
+
348
+ # contents of test_app.py
349
+ # app.py with the connection string function (prior code block)
350
+ import app
351
+
352
+
353
+ def test_connection (monkeypatch ):
354
+
355
+ # Patch the values of DEFAULT_CONFIG to specific
356
+ # testing values only for this test.
357
+ monkeypatch.setitem(app.DEFAULT_CONFIG , " user" , " test_user" )
358
+ monkeypatch.setitem(app.DEFAULT_CONFIG , " database" , " test_db" )
359
+
360
+ # expected result based on the mocks
361
+ expected = " User Id=test_user; Location=test_db;"
362
+
363
+ # the test uses the monkeypatched dictionary settings
364
+ result = app.create_connection_string()
365
+ assert result == expected
366
+
367
+ You can use the :py:meth: `monkeypatch.delitem ` to remove values.
368
+
369
+ .. code-block :: python
370
+
371
+ # contents of test_app.py
372
+ import pytest
373
+
374
+ # app.py with the connection string function
375
+ import app
376
+
377
+
378
+ def test_missing_user (monkeypatch ):
379
+
380
+ # patch the DEFAULT_CONFIG t be missing the 'user' key
381
+ monkeypatch.delitem(app.DEFAULT_CONFIG , " user" , raising = False )
382
+
383
+ # Key error expected because a config is not passed, and the
384
+ # default is now missing the 'user' entry.
385
+ with pytest.raises(KeyError ):
386
+ _ = app.create_connection_string()
387
+
388
+
389
+ The modularity of fixtures gives you the flexibility to define
390
+ separate fixtures for each potential mock and reference them in the needed tests.
391
+
392
+ .. code-block :: python
393
+
394
+ # contents of test_app.py
395
+ import pytest
396
+
397
+ # app.py with the connection string function
398
+ import app
399
+
400
+ # all of the mocks are moved into separated fixtures
401
+ @pytest.fixture
402
+ def mock_test_user (monkeypatch ):
403
+ """ Set the DEFAULT_CONFIG user to test_user."""
404
+ monkeypatch.setitem(app.DEFAULT_CONFIG , " user" , " test_user" )
405
+
406
+
407
+ @pytest.fixture
408
+ def mock_test_database (monkeypatch ):
409
+ """ Set the DEFAULT_CONFIG database to test_db."""
410
+ monkeypatch.setitem(app.DEFAULT_CONFIG , " database" , " test_db" )
411
+
412
+
413
+ @pytest.fixture
414
+ def mock_missing_default_user (monkeypatch ):
415
+ """ Remove the user key from DEFAULT_CONFIG"""
416
+ monkeypatch.delitem(app.DEFAULT_CONFIG , " user" , raising = False )
417
+
418
+
419
+ # tests reference only the fixture mocks that are needed
420
+ def test_connection (mock_test_user , mock_test_database ):
421
+
422
+ expected = " User Id=test_user; Location=test_db;"
423
+
424
+ result = app.create_connection_string()
425
+ assert result == expected
426
+
427
+
428
+ def test_missing_user (mock_missing_default_user ):
429
+
430
+ with pytest.raises(KeyError ):
431
+ _ = app.create_connection_string()
432
+
157
433
158
434
.. currentmodule :: _pytest.monkeypatch
159
435
0 commit comments