Python Mocking with unitest.mock 2
Internal
Mocking a Method with a Mock Instance
The general approach is to replace at runtime the real method instance associated with the class instance to be tested with a Mock
instance, configured to simulate various behaviors of the real method.
Assuming that our dependency to test with is SomeClass
, and this class has a some_method(nuance: str)
whose behavior we want to mock during testing, the initial implementation of the class and method could be:
class SomeClass:
def __init__(self, color: str):
self._color = color
def some_method(self, nuance: str) -> str:
return f'{nuance} {self._color}'.upper()
The normal behavior of the method some_method(nuance: str)
is reflected by:
c = SomeClass('blue')
assert c.some_method('dark') == 'DARK BLUE'
c._color = 'red'
assert c.some_method('light') == 'LIGHT RED'
We can mock the behavior of the some_method(nuance: str)
method in the following ways:
- We can return a constant value, regardless of the arguments the method is invoked with.
- We can raise an exception, regardless of the arguments the method is invoked with.
- We can replace the behavior of the method with arbitrary logic, that takes into account the arguments the method is called with.
Simulating a Particular Return Value Irrespective of the Arguments it was Called With
Configure Mock()
using the return_value
argument of its constructor so no matter how the mocked method is invoked, it will always return a constant value:
c = SomeClass('blue')
c.some_method = Mock(return_value='something completely arbitrary')
assert c.some_method('argument does not matter') == 'something completely arbitrary'
Irrespective of how the method is invoked in testing, the calling code will always get the value configured with return_value
on the mock.
This is a simplistic approach, appropriate when we don't need flexible behavior depending on the arguments. For a more nuanced approach, see Arbitrary Behavior with Access to Invocation Arguments below.
Simulating Throwing an Exception Irrespective of the Arguments it was Called With
Configure Mock()
using the side_effect
argument of its constructor, by providing an exception **instance**. Once configured as such, the mocked method will always throw exception, no matter how it is invoked.
c = SomeClass('blue')
c.some_method = Mock(side_effect=ValueError('test'))
try:
c.some_method('argument does not matter')
except ValueError as e:
assert str(e) == 'test'
An alterative syntax:
c.some_method = Mock()
c.some_method.side_effect = ValueError('test')
This is a simplistic approach, appropriate when we don't need flexible behavior depending on the arguments. For a more nuanced approach, see Arbitrary Behavior with Access to Invocation Arguments below.
Plug-in Arbitrary Behavior with Access to Invocation Arguments
The ultimate flexibility can be achieved configuring the Mock
instance with its wrap
constructor argument. wrap
specifies an object the invocation is forwarded to, with the exact same arguments the calling layer invokes the function being mocked. The invocation can be forwarded to a function or to a class.
Forwarding the Invocation to a Function
Declare a delegate function that will field the invocation sent into the mocked method. A good naming convention is to postfix the name of the method with "_mock":
def some_method_mock(nuance):
return nuance.lower()
Then configure the Mock()
instance that will replace the mocked method using the wraps
constructor argument, providing the name of the newly declared function. Careful to provide the name of the function, not to invoke the function in place:
c = SomeClass('blue')
c.some_method = Mock(wraps=some_method_mock) # NOT Mock(wraps=some_method_mock())
When the calling layer invokes into some_method()
, the arguments will be passed unchanged to the delegate function, and the calling layer will get the return value of the delegate function.
assert c.some_method('SHARP') == 'sharp'
assert c.some_method(nuance='FAINT') == 'faint'
The variables present in the scope of the delegate function will be visible from the function while invoked, giving us a way to "configure" the behavior:
MOCK_CONFIGURATION = '<>'
def some_method_mock(nuance):
return MOCK_CONFIGURATION + ' ' + nuance.lower()
c = SomeClass('blue')
c.some_method = Mock(wraps=some_method_mock)
assert c.some_method('SHARP') == '<> sharp'
Forwarding the Invocation to a Class
We can use the name of a delegate class instead the name of delegate function as argument for wraps
. In this case, the constructor of the given class will be invoked while being passed the invocation arguments, and the calling layer will get the class insurance such constructed.
class DelegateClass:
def __init__(self, nuance):
self._nuance = nuance
c = SomeClass('blue')
c.some_method = Mock(wraps=DelegateClass)
result = c.some_method('light')
assert isinstance(result, DelegateClass)
assert result._nuance == 'light'
Mocking a Method with patch()
Deplete https://kb.novaordis.com/index.php/Python_Mocking_with_unitest.mock#Mocking_a_Method