@@ -8,46 +8,215 @@ Sometimes tests need to invoke functionality which depends
88on global settings or which invokes code which cannot be easily
99tested such as network access. The ``monkeypatch `` fixture
1010helps 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+
1253See the `monkeypatch blog post `_ for some introduction material
1354and a discussion of its motivation.
1455
1556.. _`monkeypatch blog post` : http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/
1657
17-
1858Simple example: monkeypatching functions
1959----------------------------------------
2060
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" )
2489
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)
2993
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.
3496 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.
36203
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.
40204
41205Global patch example: preventing "requests" from remote operations
42206------------------------------------------------------------------
43207
44208If 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:
46210
47- # content of conftest.py
211+ .. code-block :: python
212+
213+ # contents of conftest.py
48214 import pytest
215+
216+
49217 @pytest.fixture (autouse = True )
50218 def no_requests (monkeypatch ):
219+ """ Remove requests.sessions.Session.request for all tests."""
51220 monkeypatch.delattr(" requests.sessions.Session.request" )
52221
53222 This autouse fixture will be executed for each test function and it
@@ -85,7 +254,7 @@ Monkeypatching environment variables
85254------------------------------------
86255
87256If 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
89258to do this using the ``setenv `` and ``delenv `` method. Our example code to test:
90259
91260.. code-block :: python
@@ -131,6 +300,7 @@ This behavior can be moved into ``fixture`` structures and shared across tests:
131300
132301.. code-block :: python
133302
303+ # contents of our test file e.g. test_code.py
134304 import pytest
135305
136306
@@ -144,7 +314,7 @@ This behavior can be moved into ``fixture`` structures and shared across tests:
144314 monkeypatch.delenv(" USER" , raising = False )
145315
146316
147- # Notice the tests reference the fixtures for mocks
317+ # notice the tests reference the fixtures for mocks
148318 def test_upper_to_lower (mock_env_user ):
149319 assert get_os_user_lower() == " testinguser"
150320
@@ -154,6 +324,112 @@ This behavior can be moved into ``fixture`` structures and shared across tests:
154324 _ = get_os_user_lower()
155325
156326
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+
157433
158434 .. currentmodule :: _pytest.monkeypatch
159435
0 commit comments