Python Mocking with unitest.mock 2

From NovaOrdis Knowledge Base
Jump to navigation Jump to search

Internal

Mocking a Class Instance with a Mock Instance

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

Mock Introspection

Once the mock was used and was invoked into, it is important to be able to tell whether it was used in the proper way. This is where observability comes in.

Deplete:

Asserting_Invocations_on_Mock

Mock was Used as a Target Instance

Useful accessors: method_calls, mock_calls. If the mock fielded invocations, these accessors return lists with _Call instances.

method_calls was seen empty at times, use mock_calls.

Also, when a new method invocation was sent into the Mock instance, the Mock instance "gains" a new attribute with the name of the invoked method.

Property Interaction on the Target Instance

Note that a property read or write does not count as method call, or mock call.

If a property is "written" on a mock, it can be tested naturally as follows:

target = Mock()
target.color = 'blue'
assert target.color == 'blue'

To verify that a property was not invoked, get its value and verify it's a Mock instance (if the property was set, the value would be a non-mock).

target = Mock()
# ensure a property was not invoked
assert isinstance(target.color, Mock)

Mock was Used as a Mock Method

Useful accessors: called, call_args, call_arg_list, call_count.


A _Call Instance

A _Call instance models a method invocation into a Mock instance. When a Mockinstance was used to simulate an instance being invoked into, the invocations can be introspected by accessing mock_calls or method_calls, which collect invocations as _Call instances.

Assuming that we have access to a _Call instance, then the name of the method that was called on the mock can be obtained with c[0], the positional arguments can be obtained as a tuple with c[1] or c.args, and named arguments can be obtained as a dict with c[2] or c.kwargs.

target = Mock()

target.some_method('blue', 5)
target.some_method(color='blue', size=5)

assert len(target.method_calls) == 2                  # the "calls" are collected in order, in mock.method_calls

first_recorded_call = target.method_calls[0]

assert first_recorded_call[0] == 'some_method'         # the method name
assert len(first_recorded_call[1]) == 2                # a tuple containing positional arguments
assert first_recorded_call[1][0] == 'blue'             # first positional argument
assert first_recorded_call[1][1] == 5                  # second positional argument
assert len(first_recorded_call.args) == 2              # alternative way of obtaining positional arguments
assert first_recorded_call.args[0] == 'blue'           # first positional argument
assert first_recorded_call.args[1] == 5                # second positional argument
assert len(first_recorded_call[2]) == 0                # a dict containing named arguments
assert len(first_recorded_call.kwargs) == 0            # alternative way of obtaining named arguments

second_recorded_call = target.method_calls[1]

assert second_recorded_call[0] == 'some_method'        # the method name
assert len(second_recorded_call[1]) == 0               # a tuple containing positional arguments
assert len(second_recorded_call.args) == 0             # alternative way of obtaining positional arguments
assert len(second_recorded_call[2]) == 2               # a dict containing named arguments
assert second_recorded_call[2]['color'] == 'blue'      # the 'color' named argument
assert second_recorded_call[2]['size'] == 5            # the 'size' named argument
assert len(second_recorded_call.kwargs) == 2           # alternative way of obtaining named arguments
assert second_recorded_call.kwargs['color'] == 'blue'  # the 'color' named argument
assert second_recorded_call.kwargs['size'] == 5        # the 'size' named argument