Skip to content

Commit 1e0780f

Browse files
committed
Initial commit
0 parents  commit 1e0780f

File tree

8 files changed

+128
-0
lines changed

8 files changed

+128
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__
2+
.vscode
3+
.swp

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Python unit testing: How to monkeypatch and mock methods
2+
3+
Tutorial based on this blog post [https://alexmarandon.com/articles/python_mock_gotchas/](https://alexmarandon.com/articles/python_mock_gotchas/)
4+
5+
## Problem statement
6+
Monkeypatching and mocking can be somewhat confusing in python. There is a lack of clear documentation on the topic.
7+
8+
The tests provided should give a complete view of what can and cannot be done.

data_source.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def get_name():
2+
return "Alice"

decorators.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
def noise_logger(func):
2+
def wrapped(self):
3+
result = func(self)
4+
# In a real-world scenario, the decorator would access an external
5+
# resource which we don't want our tests to depend on, such as a
6+
# caching service.
7+
print("Pet made noise: ", result)
8+
return result
9+
return wrapped

person.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from data_source import get_name
2+
from decorators import noise_logger
3+
4+
class Person(object):
5+
def __init__(self):
6+
self.pet = Pet()
7+
8+
def name(self):
9+
return get_name()
10+
11+
class Pet():
12+
@noise_logger
13+
def noise(self):
14+
return "Woof"

test_decorators.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from unittest.mock import patch
2+
patch('decorators.noise_logger', lambda x: x).start()
3+
from person import Person
4+
5+
def test_decorator():
6+
person = Person()
7+
assert person.pet.noise() == "Woof"

test_instance.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from unittest.mock import patch
2+
from person import Person
3+
import pytest
4+
5+
@pytest.mark.xfail
6+
@patch('person.Pet')
7+
def test_dog_noise_failure_to_mock(mock_pet):
8+
"""Trying to patch the instance
9+
We cannot do this. Since the class definition of `person.Pet` was defined
10+
at import time, any modifications to it will not reflect on class instances."""
11+
mock_pet.noise.return_value = "Meoow"
12+
person = Person()
13+
assert person.pet.noise() == "Meoow"
14+
15+
def test_dog_noise_with_patch_context_manager():
16+
"""We patch the module.
17+
To get the instance we must call `mock_pet.return_value`.
18+
---------> This is why they came up with patch.object
19+
"""
20+
with patch('person.Pet') as mock_pet:
21+
# If `person` is defined outside the context manager this wont work.
22+
person = Person()
23+
mock_pet.return_value.noise.return_value = "Meoow"
24+
assert person.pet.noise() == "Meoow"
25+
26+
@patch('person.Pet')
27+
def test_dog_noise_with_patch_decorator(mock_pet):
28+
"""We patch the module.
29+
To get the instance we must call `mock_pet.return_value`.
30+
---------> This is why they came up with patch.object
31+
"""
32+
person = Person()
33+
mock_pet.return_value.noise.return_value = "Meoow"
34+
assert person.pet.noise() == "Meoow"
35+
36+
def test_dog_noise_with_patch_object_context_manager():
37+
"""We patch the instance."""
38+
person = Person()
39+
with patch.object(person.pet, "noise", return_value="Meoow"):
40+
assert person.pet.noise() == "Meoow"
41+
42+
def test_dog_noise_with_monkeypatch(monkeypatch):
43+
"""We patch the instance."""
44+
person = Person()
45+
monkeypatch.setattr(person.pet, "noise", lambda: "Meoow")
46+
assert person.pet.noise() == "Meoow"

test_method_call.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from person import Person
2+
from unittest.mock import patch
3+
import pytest
4+
5+
@pytest.mark.xfail(strict=True)
6+
def test_name_with_patch_context_manager_failure():
7+
"""We are patching the method within the module.
8+
But this isn't exactly what's used by Person"""
9+
the_person = Person()
10+
with patch("data_source.get_name", return_value="Bob"):
11+
name = the_person.name()
12+
assert name == "Bob"
13+
14+
def test_name_with_patch_context_manager():
15+
"""We are patching the method within the module"""
16+
the_person = Person()
17+
with patch("person.get_name", return_value="Bob"):
18+
name = the_person.name()
19+
assert name == "Bob"
20+
21+
@patch("person.get_name")
22+
def test_name_with_patch_decorator(mock_get_name):
23+
"""We are patching the method within the module"""
24+
mock_get_name.return_value="Bob"
25+
the_person = Person()
26+
assert the_person.name() == "Bob"
27+
28+
def test_name_with_patch_object_context_manager():
29+
"""We are patching the instance we created."""
30+
the_person = Person()
31+
with patch.object(the_person, "name", return_value="Bob"):
32+
name = the_person.name()
33+
assert name == "Bob"
34+
35+
def test_name_with_monkeypatch(monkeypatch):
36+
"""We are patching the instance we created."""
37+
the_person = Person()
38+
monkeypatch.setattr(the_person, "name", lambda: "Bob")
39+
assert the_person.name() == "Bob"

0 commit comments

Comments
 (0)