Skip to content

Commit

Permalink
apply review changes
Browse files Browse the repository at this point in the history
  • Loading branch information
EvanKepner committed May 27, 2019
1 parent 7ff8e1b commit 7fa0e04
Showing 1 changed file with 51 additions and 51 deletions.
102 changes: 51 additions & 51 deletions doc/en/monkeypatch.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ functionality in tests:
All modifications will be undone after the requesting
test function or fixture has finished. The ``raising``
parameter determines if a ``KeyError`` or ``AttributeError``
will be raised if the set/deletion operation has no target.
will be raised if the target of the set/deletion operation does not exist.

Consider the following scenarios:

Expand All @@ -46,7 +46,7 @@ environment variable is missing, or to set multiple values to a known variable.
:py:meth:`monkeypatch.setenv` and :py:meth:`monkeypatch.delenv` can be used for
these patches.

4. Use :py:meth:`monkeypatch.syspath_prepend` to add to the system ``$PATH`` and
4. Use :py:meth:`monkeypatch.syspath_prepend` to modify the system ``$PATH`` and
:py:meth:`monkeypatch.chdir` to change the context of the current working directory
during a test.

Expand All @@ -59,33 +59,32 @@ Simple example: monkeypatching functions
----------------------------------------

Consider a scenario where you are working with user directories. In the context of
testing, you do not want your test to depend on the running user. ``Monkeypatch``
testing, you do not want your test to depend on the running user. ``monkeypatch``
can be used to patch functions dependent on the user to always return a
specific value.

In this example, :py:meth:`monkeypatch.setattr` is used to patch ``os.path.expanduser``
so that the known testing string ``"/abc"`` is always used when the test is run.
This removes any dependency on the ``$USER`` environment variable for testing purposes.
This removes any dependency on the running user for testing purposes.
:py:meth:`monkeypatch.setattr` must be called before the function which will use
the patched function is called.
After the test function finishes the ``os.path.expanduser`` modification will be undone.

.. code-block:: python
# Content of test_module.py with source code and the test.
# os.path is imported for reference in monkeypatch.setattr().
# Contents of test_module.py with source code and the test
# os.path is imported for reference in monkeypatch.setattr()
import os.path
def getssh():
"""Simple function to return expanded homedir ssh path from $USER"""
user = os.getenv("$USER")
return os.path.join(os.path.expanduser(f"~{user}"), ".ssh")
return os.path.expanduser("~/.ssh")
def test_getssh(monkeypatch):
# The mocked return function to replace os.path.expanduser.
# Given a path, always return '/abc'.
# The mocked return function to replace os.path.expanduser
# Given a path, always return '/abc'
def mockreturn(path):
return "/abc"
Expand All @@ -108,7 +107,7 @@ Imagine a simple function to take an API url and return the json response.

.. code-block:: python
# Contents of app.py, a simple API retrieval example.
# Contents of app.py, a simple API retrieval example
import requests
Expand All @@ -123,44 +122,44 @@ This can be done in our test file by defining a class to represent ``r``.

.. code-block:: python
# Contents of test_app.py, a simple test for our API retrieval.
# Import requests for the purposes of monkeypatching.
# Contents of test_app.py, a simple test for our API retrieval
# Import requests for the purposes of monkeypatching
import requests
# Our app.py that includes the get_json() function.
# This is the previous code block example.
# Our app.py that includes the get_json() function
# This is the previous code block example
import app
# A Custom class to be the mock return value.
# It needs to override the requests.Response returned from requests.get.
# A Custom class to be the mock return value
# It needs to override the requests.Response returned from requests.get
class MockResponse:
# The class has a json() method that we want to mock.
# The mock method always returns a specific testing dictionary.
# The class has a json() method that we want to mock
# The mock method always returns a specific testing dictionary
@staticmethod
def json():
return {"mock_key": "mock_response"}
def test_get_json(monkeypatch):
# This function is used in the monkeypatch of requests.get().
# This function is used in the monkeypatch of requests.get()
# Any arguments may be passed and it will always return our
# mocked object, which only has the .json() method.
def mock_get(*args, **kwargs):
return MockResponse()
# Apply the monkeypatch for requests.get to mock_get.
# Apply the monkeypatch for requests.get to mock_get
monkeypatch.setattr(requests, "get", mock_get)
# Our simple but illustrative test.
# app.get_json, which contains requests.get, uses the monkeypatch.
# Therefore, our result is our mock.
# Our simple but illustrative test
# app.get_json, which contains requests.get, uses the monkeypatch
# Therefore, our result is our mock
result = app.get_json("https://fakeurl")
assert result["mock_key"] == "mock_response"
``Monkeypatch`` applies the mock for ``requests.get`` with our ``mock_get`` function.
``monkeypatch`` applies the mock for ``requests.get`` with our ``mock_get`` function.
The ``mock_get`` function returns an instance of the ``MockResponse`` class, which
has a ``json()`` method defined to return a known testing dictionary and does not
require any outside API connection.
Expand All @@ -170,25 +169,25 @@ the scenario you are testing. For instance, it could include an ``ok`` property
always returns ``True``, or return different values from the ``json()`` mocked method
based on input strings.

This mock can be shared across tests using the ``fixture`` syntax:
This mock can be shared across tests using a ``fixture``:

.. code-block:: python
# Contents of test_app.py, a simple test for our API retrieval.
# Contents of test_app.py, a simple test for our API retrieval
import pytest
import requests
# Our app.py that includes the get_json() function.
# Our app.py that includes the get_json() function
import app
# A Custom class to be the mock.
# A Custom class to be the mock
class MockResponse:
@staticmethod
def json():
return {"mock_key": "mock_response"}
# Monkeypatched requests.get moved to a fixture.
# monkeypatched requests.get moved to a fixture
@pytest.fixture
def mock_response(monkeypatch):
"""Requests.get() mocked to return {'mock_key':'mock_response'}."""
Expand All @@ -199,14 +198,14 @@ This mock can be shared across tests using the ``fixture`` syntax:
monkeypatch.setattr(requests, "get", mock_get)
# Notice our test uses the custom fixture instead of monkeypatch directly.
# Notice our test uses the custom fixture instead of monkeypatch directly
def test_get_json(mock_response):
result = app.get_json("https://fakeurl")
assert result["mock_key"] == "mock_response"
Furthermore, if the mock was designed to be applied to all tests, the ``fixture`` could
be moved to ``conftest.py`` with ``autouse=True``.
be moved to a ``conftest.py`` file and use the with ``autouse=True`` option.


Global patch example: preventing "requests" from remote operations
Expand Down Expand Up @@ -261,12 +260,12 @@ Monkeypatching environment variables
------------------------------------

If you are working with environment variables you often need to safely change the values
or delete them from the system for testing purposes. ``Monkeypatch`` provides a mechanism
or delete them from the system for testing purposes. ``monkeypatch`` provides a mechanism
to do this using the ``setenv`` and ``delenv`` method. Our example code to test:

.. code-block:: python
# Contents of our original code file e.g. code.py.
# Contents of our original code file e.g. code.py
import os
Expand All @@ -286,7 +285,7 @@ both paths can be safely tested without impacting the running environment:

.. code-block:: python
# Contents of our test file e.g. test_code.py.
# Contents of our test file e.g. test_code.py
import pytest
Expand All @@ -307,6 +306,7 @@ This behavior can be moved into ``fixture`` structures and shared across tests:

.. code-block:: python
# Contents of our test file e.g. test_code.py
import pytest
Expand All @@ -320,7 +320,7 @@ This behavior can be moved into ``fixture`` structures and shared across tests:
monkeypatch.delenv("USER", raising=False)
# Notice the tests reference the fixtures for mocks.
# Notice the tests reference the fixtures for mocks
def test_upper_to_lower(mock_env_user):
assert get_os_user_lower() == "testinguser"
Expand All @@ -338,7 +338,7 @@ to specific values during tests. Take this simplified connection string example:

.. code-block:: python
# Contents of app.py to generate a simple connection string.
# Contents of app.py to generate a simple connection string
DEFAULT_CONFIG = {"user": "user1", "database": "db1"}
Expand All @@ -351,8 +351,8 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific

.. code-block:: python
# Contents of test_app.py.
# Our app.py with the connection string function (prior code block).
# Contents of test_app.py
# Our app.py with the connection string function (prior code block)
import app
Expand All @@ -363,27 +363,27 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
monkeypatch.setitem(app.DEFAULT_CONFIG, "database", "test_db")
# The expected result based on the mocks.
# The expected result based on the mocks
expected = "User Id=test_user; Location=test_db;"
# The test uses the monkeypatched dictionary settings.
# The test uses the monkeypatched dictionary settings
result = app.create_connection_string()
assert result == expected
You can use the :py:meth:`monkeypatch.delitem` to remove values.

.. code-block:: python
# Contents of test_app.py.
# Contents of test_app.py
import pytest
# Our app.py with the connection string function.
# Our app.py with the connection string function
import app
def test_missing_user(monkeypatch):
# Patch the DEFAULT_CONFIG t be missing the 'user' key.
# Patch the DEFAULT_CONFIG t be missing the 'user' key
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
# Key error expected because a config is not passed, and the
Expand All @@ -392,18 +392,18 @@ You can use the :py:meth:`monkeypatch.delitem` to remove values.
_ = app.create_connection_string()
The modularity of the ``fixture`` syntax gives you the flexibility to define
separate ``fixtures`` for each potential mock and reference them in the needed tests.
The modularity of fixtures gives you the flexibility to define
separate fixtures for each potential mock and reference them in the needed tests.

.. code-block:: python
# Contents of test_app.py.
# Contents of test_app.py
import pytest
# Our app.py with the connection string function.
# Our app.py with the connection string function
import app
# All of the mocks are moved into separated fixtures.
# All of the mocks are moved into separated fixtures
@pytest.fixture
def mock_test_user(monkeypatch):
"""Set the DEFAULT_CONFIG user to test_user."""
Expand All @@ -422,7 +422,7 @@ separate ``fixtures`` for each potential mock and reference them in the needed t
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
# Tests reference only the fixture mocks that are needed.
# Tests reference only the fixture mocks that are needed
def test_connection(mock_test_user, mock_test_database):
expected = "User Id=test_user; Location=test_db;"
Expand All @@ -431,7 +431,7 @@ separate ``fixtures`` for each potential mock and reference them in the needed t
assert result == expected
def test_missing_user(mock_missing_default_user, mock_test_database):
def test_missing_user(mock_missing_default_user):
with pytest.raises(KeyError):
_ = app.create_connection_string()
Expand Down

0 comments on commit 7fa0e04

Please sign in to comment.