-
Notifications
You must be signed in to change notification settings - Fork 13
Mock
Often after we are done writing code, we intend to check if the code is executing as intended end to end. The code written may be calling 3rd party API's or other modules of the same project. In such a case, we often intend to cross the boundaries between unit testing and integration testing by making real API calls and using real objects. Unit tests are about testing the outermost layer of the code. So, the mocks come in picture here.
Mock is the replacement of one or more function calls or objects with Mock calls or objects.A mock function call returns a predefined value immediately, without doing any work. A mock object's attributes and methods are similarly defined entirely in the test, without creating the real object or doing any work. Mocks should be handled very carefully as what value to be returned is entirely decided by the tester.
- Write test as if there were external API calls.
- Determine which calls to be mocked.(Should be a small number else consider refactoring the code)
- In the test function, patch the API Calls.
- Set Responses for these patched calls.
- Run the test.
- unittest.mock - for python 3
- Mock package - for python 2.7 (documentation same as unittest.mock)
- Mockito Library
This guide primarily covers unittest.mock and comparison between the unittest.mock package and Mockito Library.
Patch can be used as a decorator to the test function. Patch takes the name of the function that will be patched as an argument. The patch() decorator/context manager makes it easy to mock classes or objects in a module under test. The object you specify will be replaced with a mock (or other object) during the test and restored when the test ends. The mock object is then passed on to the test function. Code eg:
@patch("classA.func1")
def test_func(self,mock_func1)
If there are multiple functions to be patched then the decorator closest to the test function declaration is called first and the the first object passed onto the test function definition. Code eg:
@patch("classA.func1")
@patch("classA.func2")
def test_func(self,mock_func2,mock_func1)
While patching one needs to be very careful to what one patches. It might not be as obvious as it might seem. There are two ways in which a module is imported. Depending on the way it is imported, the patch calls are made.
When importing this way the patch call be as follows:
#my_module.py
import moduleA
def func()
@patch("moduleA.classX")
def test_func(self,mock_classX)
When importing this way, one must bear in mind that due to the semantics of the from ... import ... statement, the classes and functions are imported into the current namespace. So the patch should be:
#my_module.py
from moduleA import classX
def func()
@patch("my_module.classX")
def test_func(self,mock_classX)
By default, these arguments are instances of MagicMock, which is unittest.mock's default mocking object. You can define the behavior of the patched function by setting attributes on the returned MagicMock instance.
MagicMock objects provide a simple mocking interface that allows you to set the return value or other behavior of the function or object creation call that you patched. This allows you to fully define the behavior of the call and avoid creating real objects. We'll now discuss some attributes of MagicMock instance:
The return_value attribute on the MagicMock instance passed into your test function allows you to choose what the patched callable returns. In most cases, you'll want to return a mock version of what the callable would normally return. This can be JSON, an iterable, a value, an instance of the real response object, a MagicMock pretending to be the response object, or just about anything else. If you want to return a function consider using side_effect.
When patching objects, then it's essentially patching the object creation call. So, the return value of the MagicMock instance should be a mock object (the object of type for which original call was made but with our desired parameters). This returned object could be another MagicMock object.
classA(obj):
def myfunc(self,a,b):
...
@patch("classA.myfunc")
def test_some_func(self,mock_myfunc):
mock_myfunc.return_value = 3
print classA.myfunc(10,5)
#Output: 3
@patch("classA.myfunc")
def test_some_func(self,mock_myfunc):
mock_myfunc.return_value = 3
print classA.myfunc(10,5)
#Output: 3
Side Effect is used when you want to redirect the API call to another function, to raise an exception, handle multiple calls of the function. Code eg:
# redirect the API call to another function
def fake_func(args)
return sum(args)
@patch("classA.func1")
def test_func(self,mock_func1)
mock_func1.side_effect = fake_func
print classA.func1(1,2,3,4)
# 10
# raises an exception
@patch("classA.func1")
def test_func(self,mock_func1)
mock_func1.side_effect = KeyError
# return an iterable(i.e. return a new value each time the function is called)
@patch("classA.func1")
def test_func(self,mock_func1)
mock_func1.side_effect = [1,2,3,4,5]
print classA.func1()
# 1
print classA.func1()
# 2
print classA.func1()
# 3
print classA.func1()
# 4
# return an iterable(i.e. return a specific value each time the function is called)
vals = {(1, 2): 1, (2, 3): 2}
def side_effect_return(*args):
return vals[args]
@patch("classA.func1")
def test_func(self,mock_func1)
mock_func1.side_effect = side_effect_return
print classA.func1((1,2)
# 1
print classA.func1((2,3)
# 2
The MagicMock object behaves as if it has all the attributes of all the API's. For example- You might have to patch the plot function of the matplotlib.pyplot library but accidently patch the savefig function. The MagicMock object returned will still behave as though it has all the attributes of savefig even though we modeled it for plot function.
The solution to this is to spec the MagicMock when creating it, using the spec keyword argument: MagicMock(spec=Response). This creates a MagicMock that will only allow access to attributes and methods that are in the class from which the MagicMock is specced. Attempting to access an attribute not in the originating object will raise an AttributeError, just like the real object would. A safe usage of it is by passing an argument to the patch decorator @patch("matplotlib.pyplot.plot",autospec=True)
This is either None (if the mock hasn’t been called), or the arguments that the mock was last called with. This will be in the form of a tuple: the first member is any ordered arguments the mock was called with (or an empty tuple) and the second member is any keyword arguments (or an empty dictionary). This is very helpful when you want to use assert_called_once_with
and there are multiple arrays being passed. Example
mock_calls records all calls to the mock object, its methods, magic methods and return value mocks.
Assert that the function call being was called at least once [exactly once]. Usage not recommended. Instead use assert mock.call_count == 1
Assert that the mock was called with the specified arguments. This is especially useful when unit testing a void function.
def plot_data (data, output_directory, output_file_name):
x_data, y_data = (d for d in data)
x = np.array(x_data)
y = np.array(y_data)
plt.figure()
plt.plot(x, y, 'b-', label="Data")
plt.legend()
# plt.show()
saver.check_if_dir_exists(output_directory)
plt.savefig(output_directory + "/" + output_file_name + ".png")
plt.close()
@patch("matplotlib.pyplot.plot", autospec=True)
@patch("matplotlib.pyplot.savefig", autospec=True)
def test_plot_data(self, mock_savefig, mock_plot):
data = ([[0, 1, 2, 3, 4, 5, 6, 7, 8], [0, 2, 4, 6, 8, 10, 12, 14, 16]])
x_data, y_data = (d for d in data)
x = np.array(x_data)
y = np.array(y_data)
vis.plot_data(data, current_dir, 'test')
np.testing.assert_array_equal(x, mock_plot.call_args[0][0])
np.testing.assert_array_equal(y, mock_plot.call_args[0][1])
plt.savefig.assert_called_once_with(current_dir + "/" + "test" + ".png")
@data(([0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5], [1, 0, 1, 0]))
@unpack
@patch("lib.config.USE_PYPLOT", 1)
def test_calc_plot_linear_fit_use_pylpot(self, x_in, y_in, expected_result):
output = vis.calc_plot_linear_fit(x_in, y_in, current_dir, "linear_plot_test")
# remove the image generated from plot function
os.remove(current_dir + '/linear_plot_test.png')
assert np.allclose(output, expected_result)
- Do not check for intermediate return values in a function. Follow unittest guidelines of supplying an input and then checking the output of the function. What happens inside the function is supposed to be a black box. This ensures that if in the future the code of the function changes to use some other module but the essence of the function is still the same, then the unittest can be run directly to check if there's any flaw in the new code.
- Branch History
- Best Practices
- Testing in Python
- Logger Config
- Refactoring Suggestions