33

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?

1 Answer 1

21

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

from unittest.mock import patch

@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.

Sign up to request clarification or add additional context in comments.

7 Comments

Thanks a lot for the thorough answer. In order: 1) Good point about my case being special (that I have access to the instance et.c.). 2) Thanks for the tip about ANY. 3) Yes, perhaps I'm exaggerating the brittleness of the argument order when using decorators. 4) Question: So @patch('package.module.Class.some_method', autospec=True) is equivalent to @patch.object(package.module.Class, 'some_method', autospec=True) ? That's nice, and if so I also prefer your way of doing it. ...
... 5) I'm a bit hesitant to use setUp and tearDown as I like my tests to be completely self contained, eventhough it'll mean some more typing. But that's just a personal preference. 6) .. and on that note, yes sorry for asking a question which only has mostly subjective answers! 7) I'll consider what you said about testing private methods. I do like being able to do so easily in Python, and it's important that this code has 100% line coverage. But I'll consider breaking them out into collaborator(s). The class under test is quite small though. Again, thanks for the great answer.
Gah. Sorry for the horrible formatting. I'm really not friends with SO comment functionality. Anyway, accepting your answer as it is very good.
How many questions.... file a new question and I'll answer :).... 4) Yes are the same (I use patch.object just when I must). 5) If you did it in all your test why bother the reader to understand it at every test?.. I'm using setUp() and tearDown() just occasionally but some cases by them you can improve readability. 7) Wow 100% coverage :) ... If you use some inspect tool that will yield for private access; even if python give to you the possibility of use private method that doesn't mean it is a good thing to do even in tests....
@estan ... I used to test and sense on private data but from when I stop to do it I saw my design become better and do a little effort to expose some properties and create new class that have just one responsibility produce a very neat code.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.