I haven't used the mock library properly before, so I did some research.
There are several Python mock libraries, but this time I will use mock. In Python 3.3 or later, it is Standard Library, and when writing a program that supports only 3.3 or later Does not need to install external libraries. Actually, it seems that there are many cases where it is not supported only after 3.3, so will it be common to install with pip?
mock installation
$ pip install mock
The point is the mock.Mock class. The instance of Mock class is callable, and you can set the return value when you call.
Return value setting
>>> from mock import Mock
>>> m = Mock()
# return_Set the return value in the value attribute
>>> m.return_value = 5
>>> m()
5
Replace the Mock instance with the return value set in this way with the class method that is actually processing. Most of what you do is this.
An example is shown using the following code. In the code below, B # b_test depends on the processing of A # a_test. What if I want to test B # b_test alone?
B#b_test and A#a_test dependency
class A(object):
def a_test(self):
print('test')
class B(object):
def __init__(self, a_ins):
self.a_ins = a_ins
def b_test(self):
return self.a_ins.a_test()
In this case, you can separate the dependency by replacing A # a_test with a Mock instance.
Replacement to mock
>>> a = A()
>>> a.a_test = Mock()
>>> a.a_test.return_value = 'mocked'
>>> b = B(a)
>>> b.b_test()
'mocked'
In the above example, you have replaced the methods, but in many cases you want to replace them entirely on an instance basis, not on a method basis.
In that case, when creating a Mock instance, specify the class you want to mock in the spec
argument.
Replacement to mock(use spec)
>>> a = Mock(spec=A)
>>> a.a_test.return_value = 'mocked_spec'
>>> b = B(a)
>>> b.b_test()
'mocked_spec'
Mock calls are recorded in the Mock instance. You can use this information to check the relationships between instances (= whether the call is correct).
Record of mock calls
>>> a = Mock(spec=A)
>>> a.a_test.return_value = 'mocked_spec'
>>> b = B(a)
# Mock#call_args_list:List to store invocations for the appropriate Mock instance
>>> a.a_test.call_args_list
[]
# Mock#assert_any_call:Assert if the corresponding Mock instance call was in the past.
#In this example, there is no argument, but in reality it is possible to give any argument(Assert if there was a Mock instance call with that argument)
# (ref. http://www.voidspace.org.uk/python/mock/mock.html)
>>> a.a_test.assert_any_call()
Traceback (most recent call last):
File "<ipython-input-81-ed526bd5ddf7>", line 1, in <module>
a.a_test.assert_any_call()
File "/Users/tatsuro/.venv/py3.3/lib/python3.3/site-packages/mock.py", line 891, in assert_any_call
'%s call not found' % expected_string
AssertionError: a_test() call not found
>>> b.b_test()
'mocked_spec'
# B#b_test()Called via
>>> a.a_test.call_args_list
[call()]
>>> a.a_test.assert_any_call()
>>>
If you want to return a return value, you can set it to return_value
, but what if you want to mock the process of throwing an exception as shown below?
Processing to throw an exception
class A(object):
def a_test(self):
raise ValueError
class B(object):
def __init__(self, a_ins):
self.a_ins = a_ins
def b_test(self):
try:
return self.a_ins.a_test()
except ValueError:
print('error handling')
In that case, use Mock # side_effect
. By setting an exception here, you can throw an exception when you call the mock.
side_Exception handling mock by effect
>>> a = Mock(spec=A)
>>> a.a_test.side_effect = ValueError
>>> b = B(a)
>>> b.b_test()
error handling
Here, exception handling is mentioned as a common use case, but side_effect is not specialized for exception handling. It is a mechanism for "adding the necessary logic when calling a Mock instance", and it is also possible to add some processing only to a specific argument and use it as a return value as shown below.
Add different processing depending on the argument
>>> class B(object):
def __init__(self, a_ins):
self.a_ins = a_ins
def b_test(self, value):
try:
return self.a_ins.a_test(value)
except ValueError:
print('error handling')
>>> def handler(value):
if (value % 2) == 0:
return value * 2
return value
>>> a.a_test.side_effect = handler
>>> b = B(a)
>>> b.b_test(1)
1
>>> b.b_test(2)
4
>>> b.b_test(3)
3
However, incorporating too complicated logic into side_effect does not seem to be preferable from a maintenance point of view. If the logic becomes complicated, it may be necessary to take measures such as dividing the test case.
mock has a mechanism called patch
that enables mocking only in a specific scope.
patch
can be used in function calls, but it can also be treated as a context manager or decorator.
In many cases, it will be handled here.
An example of handling as a context manager is as follows. You can handle the mock with the name specified after as.
Treat patch as a context manager
>>> class C(object):
def hoge(self):
return 'hoge'
#Class C mock with scope only under the with statement(CMock)Can be used
>>> with patch('__main__.C') as CMock:
c = CMock()
c.hoge.return_value = 'test'
print(c.hoge())
...
test
When handling as a decorator, it is as follows. A mock is passed as the last argument of the decorated function.
Treat patch as a decorator for a function
>>> @patch('__main__.C')
def patched_func(CMock):
c = CMock()
c.hoge.return_value = 'test'
print(c.hoge())
...
>>> patched_func()
test
It can also be treated as a decorator for a class as well as a decorator for a function.
Many test tools have the ability to group test cases into classes (assuming = 1 test case / 1 method), so a common mock can be applied to all tests.
At this time, it is necessary to pay attention to the existence of patch.TEST_PREFIX
. If you specify a decorator for the class, the mock is passed only for methods that start with patch.TEST_PREFIX
.
Therefore, it is necessary to implement the test method according to a certain naming convention. (But the default value is'test', so don't you worry too much?)
Treat patch as a decorator for a class
#Mock is passed only to methods with this prefix.
>>> patch.TEST_PREFIX
'test'
>>> @patch('__main__.C')
class CTest(object):
def test_c(self, CMock):
c = CMock()
c.hoge.return_value = 'hoge_test'
print(c.hoge())
def notmock(self, CMock):
pass
...
# 'test'Methods that start with. A mock is handed over.
>>> CTest().test_c()
hoge_test
# 'test'Methods that do not start with. Since the mock is not passed, an error will occur due to insufficient arguments.
>>> CTest().notmock()
Traceback (most recent call last):
File "<ipython-input-189-cee8cb83c7b4>", line 1, in <module>
CTest().notmock()
TypeError: notmock() missing 1 required positional argument: 'CMock'
Recommended Posts