├── .gitignore ├── Makefile ├── README.md ├── myapp ├── __init__.py ├── app.py ├── lib.py └── test.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .idea 3 | *.pyc 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @. venv/bin/activate; nosetests 3 | venv: 4 | @test -d venv || virtualenv venv 5 | @. venv/bin/activate; pip install -r requirements.txt 6 | @touch venv/bin/activate 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Mock Cookbook 2 | 3 | The python [mock](https://pypi.python.org/pypi/mock) library is one of the awesome things about working in Python. No matter what code you're unit testing, it's possible to mock out various pieces with very little test code. That being said, it's sometimes difficult to figure out the exact syntax for your situation. I attribute this to the nature of how you apply the mocks. Sometimes it feel like you're TODO. 4 | 5 | The [official documentation](https://docs.python.org/3/library/unittest.mock.html) is comprehensive, but I find it somewhat hard to find what you're looking for. I recommend their [examples doc](http://www.voidspace.org.uk/python/mock/examples.html). 6 | 7 | This post is a write-up of my own personal usage. 8 | 9 | # Big Upfront Caveat 10 | 11 | The biggest mistake people make with mock is something out in the wrong place. *You always need to mock the thing where it's imported TO, not where it's imported FROM.* Translation: if you're importing `from foo import bar` into a package `bat.baz`, you need to mock it as `@mock.patch('bat.baz.bar')`. This can be confusing if you think you should be mocking it where it's defined, not where it's used. 12 | 13 | # Setup 14 | 15 | For all these sections, assume we're in a package called `myapp`. The code you're testing in a module at `myapp.app` and the definition of the objects that you're mocking is imported there from `myapp.lib`. 16 | 17 | *app.py* 18 | 19 | ```python 20 | ``` 21 | 22 | *lib.py* 23 | 24 | ```python 25 | ``` 26 | 27 | # Constants 28 | 29 | The easiest things to mock out are constants. 30 | 31 | ```python 32 | @mock.patch('myapp.app.MAX_ITEMS', 7) 33 | def test_constant(self): 34 | ... 35 | ``` 36 | 37 | # Functions 38 | 39 | For functions, you will commonly need to specify a return value, check if they were called, and with what values. 40 | 41 | ```python 42 | @mock.patch('myapp.app.get_first_name') 43 | def test_function(self, mock_get_first_name): 44 | mock_get_first_name.return_value = 'Bat' 45 | ... 46 | mock_get_first_name.assert_called() 47 | mock_get_first_name.assert_called_once_with('baz') 48 | ``` 49 | 50 | # Methods 51 | 52 | Mocking a method on a class is just like mocking a function, you just reference it through the class name. 53 | 54 | ```python 55 | @mock.patch('myapp.app.Car.get_make') 56 | def test_method(self, mock_get_make): 57 | mock_get_make.return_value = 'Ford' 58 | ... 59 | mock_get_make.assert_called() 60 | ``` 61 | 62 | # Properties 63 | 64 | These are just special methods on a class with the `@property` decorator. Now we're starting to get tricky. 65 | 66 | ```python 67 | @mock.patch('myapp.app.Car.wheels', new_callable=mock.PropertyMock) 68 | def test_property(self, mock_wheels): 69 | mock_wheels.return_value = 2 70 | ... 71 | ``` 72 | 73 | # Entire classes 74 | 75 | What if you want to swap out an entire class implementation? No problem! The key is that the `return_value` should be a new instance of the class. 76 | 77 | ```python 78 | @mock.patch('myapp.app.Car') 79 | def test_class(self, mock_car): 80 | 81 | class NewCar(object): 82 | 83 | def get_make(self): 84 | return 'Audi' 85 | 86 | @property 87 | def wheels(self): 88 | return 6 89 | 90 | mock_car.return_value = NewCar() 91 | ... 92 | ``` 93 | 94 | # Class Methods 95 | 96 | What about a `@classmethod` on a class? It's the same as a method. 97 | 98 | ```python 99 | @mock.patch('myapp.app.Car.for_make') 100 | def test_classmethod(self, mock_for_make): 101 | new_car = Car() 102 | new_car.make = 'Chevy' 103 | mock_for_make.return_value = new_car 104 | ... 105 | ``` 106 | 107 | # Static Methods 108 | 109 | Static methods are the same as class methods. 110 | 111 | ```python 112 | @mock.patch('myapp.app.Car.roll_call') 113 | def test_classmethod(self, mock_get_roll_call): 114 | mock_get_roll_call.return_value = [Car('Ford'), ] 115 | ... 116 | ``` 117 | 118 | # Decorators & Context Managers 119 | 120 | Decorators are a tough one. They are defined at import time, and are thus diffucult to re-define as a mock. Your best bet is to create a function for the body of the decorator, and mock that. 121 | 122 | Context managers are more do-able, but tricky. 123 | 124 | ```python 125 | @mock.patch('myapp.app.open_car') 126 | def test_context_manager(self, mock_open_car): 127 | 128 | def enter_car(car): 129 | pass 130 | 131 | mock_open_car.return_value.__enter__ = enter_car 132 | 133 | ... 134 | ``` 135 | 136 | # Bonus - Mocking All Tests in a Suite 137 | 138 | San you have a certain mock that you want to apply to all tests in a TestCase class. You have two options. You can apply the patch in the `setUp` and un-apply the patch in `tearDown`, or you can over-ride `run`. 139 | 140 | ```python 141 | def run(self, result=None): 142 | with mock.patch('myapp.app.foo') as foo: 143 | self.foo = foo 144 | super(MyTestCase, self).run(result) 145 | ``` 146 | -------------------------------------------------------------------------------- /myapp/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'chaseseibert' 2 | -------------------------------------------------------------------------------- /myapp/app.py: -------------------------------------------------------------------------------- 1 | from myapp.lib import MAX_ITEMS, get_first_name, Car, open_car 2 | 3 | 4 | def get_max_items(): 5 | return MAX_ITEMS 6 | 7 | 8 | def get_full_name(arg): 9 | return ' '.join((get_first_name(arg), 'Bar')) 10 | 11 | 12 | def get_car_make(make=None): 13 | car = Car() 14 | if make: 15 | car = Car.for_make(make) 16 | return car.get_make() 17 | 18 | 19 | def get_car_wheels(): 20 | return Car().wheels 21 | 22 | 23 | def get_roll_call(): 24 | return Car.roll_call() 25 | 26 | 27 | def close_car(car): 28 | states = [] 29 | with open_car(car) as car: 30 | states.append(car.closed) 31 | states.append(car.closed) 32 | return states 33 | -------------------------------------------------------------------------------- /myapp/lib.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | 3 | 4 | MAX_ITEMS = 10 5 | 6 | 7 | def get_first_name(arg): 8 | return arg or "Foo" 9 | 10 | 11 | class Car(object): 12 | 13 | def __init__(self, make=None): 14 | self.make = make 15 | self.closed = False 16 | 17 | @classmethod 18 | def for_make(cls, make): 19 | car = cls() 20 | car.make = make 21 | return car 22 | 23 | def get_make(self): 24 | return self.make 25 | 26 | @property 27 | def wheels(self): 28 | return 4 29 | 30 | @staticmethod 31 | def roll_call(): 32 | return [Car('Ford'), Car('Chevy'), Car('BMW'), Car('Audi')] 33 | 34 | def close(self): 35 | self.closed = True 36 | 37 | def __repr__(self): 38 | return '' % self.make 39 | 40 | def __eq__(self, other): 41 | return self.make == other.make 42 | 43 | 44 | @contextmanager 45 | def open_car(car): 46 | yield car 47 | car.close() 48 | -------------------------------------------------------------------------------- /myapp/test.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import mock 3 | from unittest import TestCase 4 | 5 | from myapp.app import get_max_items, get_full_name, get_car_make, get_car_wheels, get_roll_call, \ 6 | close_car 7 | from myapp.lib import Car 8 | 9 | 10 | class MyTests(TestCase): 11 | 12 | @mock.patch('myapp.app.MAX_ITEMS', 7) 13 | def test_constant(self): 14 | self.assertEquals(get_max_items(), 7) 15 | 16 | @mock.patch('myapp.app.get_first_name') 17 | def test_function(self, mock_get_first_name): 18 | mock_get_first_name.return_value = 'Bat' 19 | self.assertEquals(get_full_name('baz'), 'Bat Bar') 20 | mock_get_first_name.assert_called() 21 | mock_get_first_name.assert_called_once_with('baz') 22 | 23 | @mock.patch('myapp.app.Car.get_make') 24 | def test_method(self, mock_get_make): 25 | mock_get_make.return_value = 'Ford' 26 | self.assertEquals(get_car_make(), 'Ford') 27 | mock_get_make.assert_called() 28 | 29 | @mock.patch('myapp.app.Car.wheels', new_callable=mock.PropertyMock) 30 | def test_property(self, mock_wheels): 31 | mock_wheels.return_value = 2 32 | self.assertEquals(get_car_wheels(), 2) 33 | 34 | @mock.patch('myapp.app.Car') 35 | def test_class(self, mock_car): 36 | 37 | class NewCar(object): 38 | 39 | def get_make(self): 40 | return 'Audi' 41 | 42 | @property 43 | def wheels(self): 44 | return 6 45 | 46 | mock_car.return_value = NewCar() 47 | self.assertEquals(get_car_make(), 'Audi') 48 | self.assertEquals(get_car_wheels(), 6) 49 | 50 | @mock.patch('myapp.app.Car.roll_call') 51 | def test_classmethod(self, mock_get_roll_call): 52 | mock_get_roll_call.return_value = [Car('Ford'), ] 53 | self.assertEquals(get_roll_call(), [Car('Ford'), ]) 54 | 55 | @mock.patch('myapp.app.open_car') 56 | def test_context_manager(self, mock_open_car): 57 | 58 | car = Car() 59 | car.closed = 'Foo' 60 | 61 | def enter_car(car): 62 | car.closed = 'Bar' 63 | return car 64 | 65 | mock_open_car.return_value.__enter__ = enter_car 66 | #mock_open_car.return_value.__exit__ = exit_car 67 | 68 | states = close_car(car) 69 | self.assertEquals(states, ['Bar', 'Bar']) 70 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mock==1.0.1 2 | nose==1.3.7 3 | --------------------------------------------------------------------------------