|
5 | 5 | ----------
|
6 | 6 | 问题
|
7 | 7 | ----------
|
8 |
| -You’re writing unit tests and need to apply patches to selected objects in order to make |
9 |
| -assertions about how they were used in the test (e.g., assertions about being called with |
10 |
| -certain parameters, access to selected attributes, etc.). |
| 8 | +你写的单元测试中需要给指定的对象打补丁, |
| 9 | +用来断言它们在测试中的期望行为(比如,断言被调用时的参数个数,访问指定的属性等)。 |
11 | 10 |
|
12 | 11 | |
|
13 | 12 |
|
14 | 13 | ----------
|
15 | 14 | 解决方案
|
16 | 15 | ----------
|
17 |
| -The unittest.mock.patch() function can be used to help with this problem. It’s a little |
18 |
| -unusual, but patch() can be used as a decorator, a context manager, or stand-alone. For |
19 |
| -example, here’s an example of how it’s used as a decorator: |
| 16 | +``unittest.mock.patch()`` 函数可被用来解决这个问题。 |
| 17 | +``patch()`` 还可被用作一个装饰器、上下文管理器或单独使用,尽管并不常见。 |
| 18 | +例如,下面是一个将它当做装饰器使用的例子: |
20 | 19 |
|
21 |
| -from unittest.mock import patch |
22 |
| -import example |
| 20 | +.. code-block:: python |
23 | 21 |
|
24 |
| -@patch('example.func') |
25 |
| -def test1(x, mock_func): |
26 |
| - example.func(x) # Uses patched example.func |
27 |
| - mock_func.assert_called_with(x) |
28 |
| -It can also be used as a context manager: |
| 22 | + from unittest.mock import patch |
| 23 | + import example |
29 | 24 |
|
30 |
| -with patch('example.func') as mock_func: |
31 |
| - example.func(x) # Uses patched example.func |
32 |
| - mock_func.assert_called_with(x) |
| 25 | + @patch('example.func') |
| 26 | + def test1(x, mock_func): |
| 27 | + example.func(x) # Uses patched example.func |
| 28 | + mock_func.assert_called_with(x) |
33 | 29 |
|
34 |
| -Last, but not least, you can use it to patch things manually: |
| 30 | +它还可以被当做一个上下文管理器: |
35 | 31 |
|
36 |
| -p = patch('example.func') |
37 |
| -mock_func = p.start() |
38 |
| -example.func(x) |
39 |
| -mock_func.assert_called_with(x) |
40 |
| -p.stop() |
| 32 | +.. code-block:: python |
41 | 33 |
|
42 |
| -If necessary, you can stack decorators and context managers to patch multiple objects. |
43 |
| -For example: |
| 34 | + with patch('example.func') as mock_func: |
| 35 | + example.func(x) # Uses patched example.func |
| 36 | + mock_func.assert_called_with(x) |
44 | 37 |
|
45 |
| -@patch('example.func1') |
46 |
| -@patch('example.func2') |
47 |
| -@patch('example.func3') |
48 |
| -def test1(mock1, mock2, mock3): |
49 |
| - ... |
| 38 | +最后,你还可以手动的使用它打补丁: |
50 | 39 |
|
51 |
| -def test2(): |
52 |
| - with patch('example.patch1') as mock1, \ |
53 |
| - patch('example.patch2') as mock2, \ |
54 |
| - patch('example.patch3') as mock3: |
55 |
| - ... |
| 40 | +.. code-block:: python |
| 41 | +
|
| 42 | + p = patch('example.func') |
| 43 | + mock_func = p.start() |
| 44 | + example.func(x) |
| 45 | + mock_func.assert_called_with(x) |
| 46 | + p.stop() |
| 47 | +
|
| 48 | +如果可能的话,你能够叠加装饰器和上下文管理器来给多个对象打补丁。例如: |
| 49 | + |
| 50 | +.. code-block:: python |
| 51 | +
|
| 52 | + @patch('example.func1') |
| 53 | + @patch('example.func2') |
| 54 | + @patch('example.func3') |
| 55 | + def test1(mock1, mock2, mock3): |
| 56 | + ... |
| 57 | +
|
| 58 | + def test2(): |
| 59 | + with patch('example.patch1') as mock1, \ |
| 60 | + patch('example.patch2') as mock2, \ |
| 61 | + patch('example.patch3') as mock3: |
| 62 | + ... |
56 | 63 |
|
57 | 64 | |
|
58 | 65 |
|
59 | 66 | ----------
|
60 | 67 | 讨论
|
61 | 68 | ----------
|
62 |
| -patch() works by taking an existing object with the fully qualified name that you pro‐ |
63 |
| -vide and replacing it with a new value. The original value is then restored after the |
64 |
| -completion of the decorated function or context manager. By default, values are replaced |
65 |
| -with MagicMock instances. For example: |
66 |
| - |
67 |
| ->>> x = 42 |
68 |
| ->>> with patch('__main__.x'): |
69 |
| -... print(x) |
70 |
| -... |
71 |
| -<MagicMock name='x' id='4314230032'> |
72 |
| ->>> x |
73 |
| -42 |
74 |
| ->>> |
75 |
| - |
76 |
| -However, you can actually replace the value with anything that you wish by supplying |
77 |
| -it as a second argument to patch(): |
78 |
| - |
79 |
| ->>> x |
80 |
| -42 |
81 |
| ->>> with patch('__main__.x', 'patched_value'): |
82 |
| -... print(x) |
83 |
| -... |
84 |
| -patched_value |
85 |
| ->>> x |
86 |
| -42 |
87 |
| ->>> |
88 |
| - |
89 |
| -The MagicMock instances that are normally used as replacement values are meant to |
90 |
| -mimic callables and instances. They record information about usage and allow you to |
91 |
| -make assertions. For example: |
92 |
| - |
93 |
| ->>> from unittest.mock import MagicMock |
94 |
| ->>> m = MagicMock(return_value = 10) |
95 |
| ->>> m(1, 2, debug=True) |
96 |
| -10 |
97 |
| ->>> m.assert_called_with(1, 2, debug=True) |
98 |
| ->>> m.assert_called_with(1, 2) |
99 |
| -Traceback (most recent call last): |
100 |
| - File "<stdin>", line 1, in <module> |
101 |
| - File ".../unittest/mock.py", line 726, in assert_called_with |
102 |
| - raise AssertionError(msg) |
103 |
| -AssertionError: Expected call: mock(1, 2) |
104 |
| -Actual call: mock(1, 2, debug=True) |
105 |
| ->>> |
106 |
| - |
107 |
| ->>> m.upper.return_value = 'HELLO' |
108 |
| ->>> m.upper('hello') |
109 |
| -'HELLO' |
110 |
| ->>> assert m.upper.called |
111 |
| - |
112 |
| ->>> m.split.return_value = ['hello', 'world'] |
113 |
| ->>> m.split('hello world') |
114 |
| -['hello', 'world'] |
115 |
| ->>> m.split.assert_called_with('hello world') |
116 |
| ->>> |
117 |
| - |
118 |
| ->>> m['blah'] |
119 |
| -<MagicMock name='mock.__getitem__()' id='4314412048'> |
120 |
| ->>> m.__getitem__.called |
121 |
| -True |
122 |
| ->>> m.__getitem__.assert_called_with('blah') |
123 |
| ->>> |
124 |
| - |
125 |
| -Typically, these kinds of operations are carried out in a unit test. For example, suppose |
126 |
| -you have some function like this: |
127 |
| - |
128 |
| -# example.py |
129 |
| -from urllib.request import urlopen |
130 |
| -import csv |
131 |
| - |
132 |
| -def dowprices(): |
133 |
| - u = urlopen('http://finance.yahoo.com/d/quotes.csv?s=@^DJI&f=sl1') |
134 |
| - lines = (line.decode('utf-8') for line in u) |
135 |
| - rows = (row for row in csv.reader(lines) if len(row) == 2) |
136 |
| - prices = { name:float(price) for name, price in rows } |
137 |
| - return prices |
138 |
| - |
139 |
| -Normally, this function uses urlopen() to go fetch data off the Web and parse it. To |
140 |
| -unit test it, you might want to give it a more predictable dataset of your own creation, |
141 |
| -however. Here’s an example using patching: |
142 |
| - |
143 |
| -import unittest |
144 |
| -from unittest.mock import patch |
145 |
| -import io |
146 |
| -import example |
147 |
| - |
148 |
| -sample_data = io.BytesIO(b'''\ |
149 |
| -"IBM",91.1\r |
150 |
| -"AA",13.25\r |
151 |
| -"MSFT",27.72\r |
152 |
| -\r |
153 |
| -''') |
154 |
| - |
155 |
| -class Tests(unittest.TestCase): |
156 |
| - @patch('example.urlopen', return_value=sample_data) |
157 |
| - def test_dowprices(self, mock_urlopen): |
158 |
| - p = example.dowprices() |
159 |
| - self.assertTrue(mock_urlopen.called) |
160 |
| - self.assertEqual(p, |
161 |
| - {'IBM': 91.1, |
162 |
| - 'AA': 13.25, |
163 |
| - 'MSFT' : 27.72}) |
164 |
| - |
165 |
| -if __name__ == '__main__': |
166 |
| - unittest.main() |
167 |
| - |
168 |
| -In this example, the urlopen() function in the example module is replaced with a mock |
169 |
| -object that returns a BytesIO() containing sample data as a substitute. |
170 |
| -An important but subtle facet of this test is the patching of example.urlopen instead of |
171 |
| -urllib.request.urlopen. When you are making patches, you have to use the names |
172 |
| -as they are used in the code being tested. Since the example code uses from urllib.re |
173 |
| -quest import urlopen, the urlopen() function used by the dowprices() function is |
174 |
| -actually located in example. |
175 |
| -This recipe has really only given a very small taste of what’s possible with the uni |
176 |
| -ttest.mock module. The official documentation is a must-read for more advanced |
177 |
| -features. |
| 69 | +``patch()`` 接受一个已存在对象的全路径名,将其替换为一个新的值。 |
| 70 | +原来的值会在装饰器函数或上下文管理器完成后自动恢复回来。 |
| 71 | +默认情况下,所有值会被 ``MagicMock`` 实例替代。例如: |
| 72 | + |
| 73 | +.. code-block:: python |
| 74 | +
|
| 75 | + >>> x = 42 |
| 76 | + >>> with patch('__main__.x'): |
| 77 | + ... print(x) |
| 78 | + ... |
| 79 | + <MagicMock name='x' id='4314230032'> |
| 80 | + >>> x |
| 81 | + 42 |
| 82 | + >>> |
| 83 | +
|
| 84 | +不过,你可以通过给 ``patch()`` 提供第二个参数来将值替换成任何你想要的: |
| 85 | + |
| 86 | +.. code-block:: python |
| 87 | +
|
| 88 | + >>> x |
| 89 | + 42 |
| 90 | + >>> with patch('__main__.x', 'patched_value'): |
| 91 | + ... print(x) |
| 92 | + ... |
| 93 | + patched_value |
| 94 | + >>> x |
| 95 | + 42 |
| 96 | + >>> |
| 97 | +
|
| 98 | +被用来作为替换值的 ``MagicMock`` 实例能够模拟可调用对象和实例。 |
| 99 | +他们记录对象的使用信息并允许你执行断言检查,例如: |
| 100 | + |
| 101 | +.. code-block:: python |
| 102 | +
|
| 103 | + >>> from unittest.mock import MagicMock |
| 104 | + >>> m = MagicMock(return_value = 10) |
| 105 | + >>> m(1, 2, debug=True) |
| 106 | + 10 |
| 107 | + >>> m.assert_called_with(1, 2, debug=True) |
| 108 | + >>> m.assert_called_with(1, 2) |
| 109 | + Traceback (most recent call last): |
| 110 | + File "<stdin>", line 1, in <module> |
| 111 | + File ".../unittest/mock.py", line 726, in assert_called_with |
| 112 | + raise AssertionError(msg) |
| 113 | + AssertionError: Expected call: mock(1, 2) |
| 114 | + Actual call: mock(1, 2, debug=True) |
| 115 | + >>> |
| 116 | +
|
| 117 | + >>> m.upper.return_value = 'HELLO' |
| 118 | + >>> m.upper('hello') |
| 119 | + 'HELLO' |
| 120 | + >>> assert m.upper.called |
| 121 | +
|
| 122 | + >>> m.split.return_value = ['hello', 'world'] |
| 123 | + >>> m.split('hello world') |
| 124 | + ['hello', 'world'] |
| 125 | + >>> m.split.assert_called_with('hello world') |
| 126 | + >>> |
| 127 | +
|
| 128 | + >>> m['blah'] |
| 129 | + <MagicMock name='mock.__getitem__()' id='4314412048'> |
| 130 | + >>> m.__getitem__.called |
| 131 | + True |
| 132 | + >>> m.__getitem__.assert_called_with('blah') |
| 133 | + >>> |
| 134 | +
|
| 135 | +一般来讲,这些操作会在一个单元测试中完成。例如,假设你已经有了像下面这样的函数: |
| 136 | + |
| 137 | +.. code-block:: python |
| 138 | +
|
| 139 | + # example.py |
| 140 | + from urllib.request import urlopen |
| 141 | + import csv |
| 142 | +
|
| 143 | + def dowprices(): |
| 144 | + u = urlopen('http://finance.yahoo.com/d/quotes.csv?s=@^DJI&f=sl1') |
| 145 | + lines = (line.decode('utf-8') for line in u) |
| 146 | + rows = (row for row in csv.reader(lines) if len(row) == 2) |
| 147 | + prices = { name:float(price) for name, price in rows } |
| 148 | + return prices |
| 149 | +
|
| 150 | +正常来讲,这个函数会使用 ``urlopen()`` 从Web上面获取数据并解析它。 |
| 151 | +在单元测试中,你可以给它一个预先定义好的数据集。下面是使用补丁操作的例子: |
| 152 | + |
| 153 | +.. code-block:: python |
| 154 | +
|
| 155 | + import unittest |
| 156 | + from unittest.mock import patch |
| 157 | + import io |
| 158 | + import example |
| 159 | +
|
| 160 | + sample_data = io.BytesIO(b'''\ |
| 161 | + "IBM",91.1\r |
| 162 | + "AA",13.25\r |
| 163 | + "MSFT",27.72\r |
| 164 | + \r |
| 165 | + ''') |
| 166 | +
|
| 167 | + class Tests(unittest.TestCase): |
| 168 | + @patch('example.urlopen', return_value=sample_data) |
| 169 | + def test_dowprices(self, mock_urlopen): |
| 170 | + p = example.dowprices() |
| 171 | + self.assertTrue(mock_urlopen.called) |
| 172 | + self.assertEqual(p, |
| 173 | + {'IBM': 91.1, |
| 174 | + 'AA': 13.25, |
| 175 | + 'MSFT' : 27.72}) |
| 176 | +
|
| 177 | + if __name__ == '__main__': |
| 178 | + unittest.main() |
| 179 | +
|
| 180 | +本例中,位于 ``example`` 模块中的 ``urlopen()`` 函数被一个模拟对象替代, |
| 181 | +该对象会返回一个包含测试数据的 ``ByteIO()``. |
| 182 | + |
| 183 | +还有一点,在打补丁时我们使用了 ``example.urlopen`` 来代替 ``urllib.request.urlopen`` 。 |
| 184 | +当你创建补丁的时候,你必须使用它们在测试代码中的名称。 |
| 185 | +由于测试代码使用了 ``from urllib.request import urlopen`` ,那么 ``dowprices()`` 函数 |
| 186 | +中使用的 ``urlopen()`` 函数实际上就位于 ``example`` 模块了。 |
| 187 | + |
| 188 | +本节实际上只是对 ``unittest.mock`` 模块的一次浅尝辄止。 |
| 189 | +更多更高级的特性,请参考 `官方文档 <http://docs.python.org/3/library/unittest.mock>`_ |
0 commit comments