Preferred way of patching multiple methods in Python unit test

I need to patch three methods ( _send_reply , _reset_watchdog and _handle_set_watchdog ) with mock methods before testing a call to a fourth method ( _handle_command ) in a unit test of mine.

From looking at the documentation for the mock package, there's a few ways I could go about it:

With patch.multiple as decorator

@patch.multiple(MBG120Simulator,
                _send_reply=DEFAULT,
                _reset_watchdog=DEFAULT,
                _handle_set_watchdog=DEFAULT,
                autospec=True)
def test_handle_command_too_short_v1(self,
                                     _send_reply,
                                     _reset_watchdog,
                                     _handle_set_watchdog):
    simulator = MBG120Simulator()
    simulator._handle_command('XA99')
    _send_reply.assert_called_once_with(simulator, 'X?')
    self.assertFalse(_reset_watchdog.called)
    self.assertFalse(_handle_set_watchdog.called)
    simulator.stop()

With patch.multiple as context manager

def test_handle_command_too_short_v2(self):
    simulator = MBG120Simulator()

    with patch.multiple(simulator,
                        _send_reply=DEFAULT,
                        _reset_watchdog=DEFAULT,
                        _handle_set_watchdog=DEFAULT,
                        autospec=True) as mocks:
        simulator._handle_command('XA99')
        mocks['_send_reply'].assert_called_once_with('X?')
        self.assertFalse(mocks['_reset_watchdog'].called)
        self.assertFalse(mocks['_handle_set_watchdog'].called)
        simulator.stop()

With multiple patch.object decoratorations

@patch.object(MBG120Simulator, '_send_reply', autospec=True)
@patch.object(MBG120Simulator, '_reset_watchdog', autospec=True)
@patch.object(MBG120Simulator, '_handle_set_watchdog', autospec=True)
def test_handle_command_too_short_v3(self,
                                     _handle_set_watchdog_mock,
                                     _reset_watchdog_mock,
                                     _send_reply_mock):
    simulator = MBG120Simulator()
    simulator._handle_command('XA99')
    _send_reply_mock.assert_called_once_with(simulator, 'X?')
    self.assertFalse(_reset_watchdog_mock.called)
    self.assertFalse(_handle_set_watchdog_mock.called)
    simulator.stop()

Manually replacing methods using create_autospec

def test_handle_command_too_short_v4(self):
    simulator = MBG120Simulator()

    # Mock some methods.
    simulator._send_reply = create_autospec(simulator._send_reply)
    simulator._reset_watchdog = create_autospec(simulator._reset_watchdog)
    simulator._handle_set_watchdog = create_autospec(simulator._handle_set_watchdog)

    # Exercise.
    simulator._handle_command('XA99')

    # Check.
    simulator._send_reply.assert_called_once_with('X?')
    self.assertFalse(simulator._reset_watchdog.called)
    self.assertFalse(simulator._handle_set_watchdog.called)

Personally I think the last one is clearest to read, and will not result in horribly long lines if the number of mocked methods grow. It also avoids having to pass in simulator as the first ( self ) argument to assert_called_once_with .

But I don't find any of them particularly nice. Especially the multiple patch.object approach, which requires careful matching of the parameter order to the nested decorations.

Is there some approach I've missed, or a way to make this more readable? What do you do when you need to patch multiple methods on the instance/class under test?


No you didn't have missed anything really different from what you proposed.

About readability my taste is for decorator way because it remove the mocking stuff from test body... but it is just taste.

You are right: if you patch the static instance of the method by autospec=True you must use self in assert_called_* family check methods. But your case is just a small class because you know exactly what object you need to patch and you don't really need other context for your patch than test method.

You need just patch your object use it for all your test: often in tests you cannot have the instance to patch before doing your call and in these cases create_autospec cannot be used: you can just patch the static instance of the methods instead.

If you are bothered by passing the instance to assert_called_* methods consider to use ANY to break the dependency. Finally I wrote hundreds of test like that and I never had a problem about the arguments order.

My standard approach at your test is

@patch('mbgmodule.MBG120Simulator._send_reply', autospec=True)
@patch('mbgmodule.MBG120Simulator._reset_watchdog', autospec=True)
@patch('mbgmodule.MBG120Simulator._handle_set_watchdog', autospec=True)
def test_handle_command_too_short(self,mock_handle_set_watchdog,
                                          mock_reset_watchdog,
                                          mock_send_reply):
    simulator = MBG120Simulator()
    simulator._handle_command('XA99')
    # You can use ANY instead simulator if you don't know it
    mock_send_reply.assert_called_once_with(simulator, 'X?')
    self.assertFalse(mock_reset_watchdog.called)
    self.assertFalse(mock_handle_set_watchdog_mock.called)
    simulator.stop()
  • Patching is out of the test method code
  • Every mock starts by mock_ prefix
  • I prefer to use simple patch call and absolute path: it is clear and neat what you are doing
  • Finally: maybe create simulator and stop it are setUp() and tearDown() responsibility and tests should take in account just to patch some methods and do the checks.

    I hope that answer is useful but the question don't have a unique valid answer because readability is not an absolute concept and depends from the reader. Moreover even the title speaking about general case, question examples are about the specific class of problem where you should patch methods of the object to test.

    [EDIT]

    I though a while about this question and I found what bother me: you are trying to test and sense on private methods. When this happen the first thing that you should ask is why? There are a lot chances that the answer is because these methods should be public methods of private collaborators (that not my words).

    In that new scenario you should sense on private collaborators and you cannot change just your object. What you need to do is to patch the static instance of some other classes.

    链接地址: http://www.djcxy.com/p/84244.html

    上一篇: 检查fstream是文件还是目录

    下一篇: 在Python单元测试中修补多个方法的首选方法