The Ultimate Guide To Using Pytest Monkeypatch with 2 Code Examples

Have you heard of monkeypatch but have no idea what it means?

Or maybe you’ve even seen it in your company’s code base and wondered why your colleagues use it.

Rather than go through complex documentation or a bunch of Stack Overflow posts, let’s understand what is monkeypatching at a high level and when and how we can use it to improve Unit Testing.

We’ll look at a real example that you can test in your IDE/Terminal, understand the concept and confidently start applying it to production code where you deem necessary.

In this article, we’ll talk about

  • What Is Monkeypatching?

  • Where Is Monkeypatching Used?

    • Calling an External API — Using HTTP Requests
    • Mock Delays, Auth, DB Connection Objects Or Other Dependencies
  • Using Monkeypatch

    • Real Example 1 (Calculate the sum of 2 numbers)
    • Real Example 2 (Get random cat fact via Requests)
  • Is MonkeyPatch The Same As Mocking or Patching?

  • Conclusion

  • Additional Reading

So let’s begin.

Link To GitHub Repo

What Is Monkeypatching?

So what exactly is Monkeypatching?

And no, it has nothing to do with monkeys (nope, I don’t know where the name came from).

Monkeypatching is a term for mocking or patching a piece of code (class, function, module, variable or object) as part of a Unit Test.

Why would you want to do that?

Often you want to mask the inner workings of your code and just test specific functionality.

Where Is Monkeypatching Used?

A variety of use cases.

Calling an External API — Using HTTP Requests

In our article on Python REST API Unit Testing, we explored ways to Unit Test calls to the DogAPI.

Making calls to external APIs can be expensive and resource intensive. A fantastic use case to mock or patch the response of the API being called.

This way we can test how our code handles the response and move on, without any execution time delays or dependency on the External API.

Mock Delays, Auth, DB Connection Objects Or Other Dependencies

Maybe there are other bits of dependency code we wish to bypass for test reasons. These could include — Authentication, delays, database connection objects or others.

Using Monkeypatch

Real Example 1 (Calculate sum of 2 numbers)

Let’s move beyond the talk and look at a basic real example of how we can use monkeypatch.

calculate_sum.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def calculate_sum(a: int | float, b: int | float) -> str:
"""
Function to calculate sum of 2 numbers.
:param a: Number 1, Type - int/float
:param b: Number 2, Type - int/float
:return: String with the sum of the 2 numbers
"""
delay()
return f"Sum of the 2 Numbers is `{a + b}`"


def delay():
"""
Function to introduce a 5-second delay
:return:
"""
print("5 Sec Delay....")
time.sleep(5)

In this example module, we have 2 very basic functions.

  1. calculate_sum — Calculates the sum of 2 numbers (float or int).
  2. delay — Introduces a 5-second delay and is called by the calculate_sum function, just before printing the output.

If you navigate to the /tests folder you’ll see a unit test — test_calculate_sum.py with 2 main tests.

test_calculate_sum_no_monkeypatch  (Keep Delay)

1
2
3
4
5
6
7
def test_calculate_sum_no_monkeypatch() -> None:
"""
Tests to calculate sum with NO Monkeypatch
:return: None
"""
x = calculate_sum(2, 2)
assert x == "Sum of the 2 Numbers is `4`"

In this case, we allow the 5-second delay to run. Which inadvertently takes longer to run our overall suite of Unit Tests.

test_calculate_sum_w_monkeypatch (Using Monkeypatch to eliminate delay)

1
2
3
4
5
6
7
8
9
10
11
12
13
def test_calculate_sum_w_monkeypatch(monkeypatch) -> None:
"""
Test to calculate sum WITH monkeypatch (delay function)
:param monkeypatch:
:return: None
"""
def mock_return():
return print("NO 5 Sec Delay!!!")

monkeypatch.setattr("monkeypatch_examples.calculate_sum.delay", mock_return)

x = calculate_sum(2, 2)
assert x == "Sum of the 2 Numbers is `4`"

In the second case, we monkeypatch the delay function using mock_return() and can make the patch return whatever we want.

In this example, I’ve made it print the string “NO 5 Sec Delay!!!”

So you can see how powerful this actually is.

Real Example 2 (Get random cat fact via Requests Library)

OK, now let’s take a look at another use case, one which we get a random cat fact from the meowfacts API.

cat_fact.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
base_url = "https://meowfacts.herokuapp.com/"


def get_cat_fact() -> tuple[int, dict] | str:
try:
response = requests.get(base_url)
if response.status_code in (200, 201):
return response.status_code, response.json()
else:
return json.dumps({"ERROR": "Cat Fact Not Available"})
except requests.exceptions.HTTPError as errh:
logging.error(errh)
except requests.exceptions.ConnectionError as errc:
logging.error(errc)
except requests.exceptions.Timeout as errt:
logging.error(errt)
except requests.exceptions.RequestException as err:
logging.error(err)

In this example, we’re calling the meowfacts` API, which returns a random cat fact on every call.

We do this via the get_cat_fact() method using the Requests library.

Now let’s look at the Unit Test.

In this test case, we have 2 Unit Tests

test_cat_fact_no_monkeypatch()

1
2
3
def test_cat_fact_no_monkeypatch():
code, response = get_cat_fact()
assert code == 200

As you might have guessed, it’s tough to write an assert statement when we don’t know what the cat fact will be.

We can only test the API response code.

As discussed in another article on Python REST API Unit Testing, this requires a real call to the API which can be time and money intensive.

Especially if you wanna do large-scale performance testing. Enter Monkeypatching :)

test_cat_fact_w_monkeypatch()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def test_cat_fact_w_monkeypatch(monkeypatch):
class MockResponse(object):
def __init__(self):
self.status_code = 200
self.url = "www.testurl.com"

def json(self):
return {'data': ['Mother cats teach their '
'kittens to use the litter box.']}

def mock_get(*args, **kwargs):
return MockResponse()

monkeypatch.setattr(requests, 'get', mock_get)
assert get_cat_fact() == (200, {'data': ['Mother cats '
'teach their kittens '
'to use the litter box.']})

Here’s where the real power of monkeypatching comes to light.

We can mock or simulate the Cat Fact REST API response

In this case, we define a class MockResponse and force it to return a JSON response, essentially simulating the Requests.GET method.

You can see how this can be incredibly helpful for large-scale Unit and Integration testing.

Is MonkeyPatch The Same As Mocking or Patching?

Yes and No ;)

The two are very similar and have subtle differences.

Monkeypatching is the act of replacing a function, method, class or variable at runtime.

Mock actually uses Monkeypatch under the hood to mock or change certain objects being evaluated at run time as part of your test.

The exact differences are not really important, what’s more important is that you understand that it’s possible to override functions, classes, libraries and variables in Unit Tests.

The below Stack Overflow post gives an insight into some of the differences.

What is the difference between mocking and monkey patching?

Conclusion

I hope this article has been useful.

We looked at the definition of Monkeypatching and how it can be used to simulate various parts of code functionality, incredibly useful in Unit Testing.

We then looked at a couple of real examples of how to apply monkeypatch to simplify Unit Testing

  1. Calculate the sum of 2 numbers
  2. Get a random cat fact

Lastly we looked at some subtle differences between Monkeypatching and mocking and what it means.

But the Key Takeaway is that you can modify bits of code at run time which can save valuable testing resources.

In a subsequent update, we’ll cover — How to use monkeypatch with the popular Hypothesis testing library

We’ll also break down each of the monkeypatching helper methods and understand how to use them.

Till the next time… Cheers!