2

I'm trying to use @patch decorators and mock objects to test the behavior of my code consuming some libraries. Unfortunately, I can't see any way to do what I'm trying to do using only decorators, and it seems like I should be able to. I realize that generally I should be more concerned with the return value of a method than the particular sequence of calls it makes to a library, but it's very difficult to write a test to compare one image to another.

I thought that maybe I could create instances of my mock objects and then use @patch.object(...) but that didn't work, and obviously @patch('MockObject.method') will only mock class methods. I can get the code to work by using with patch.object(someMock.return_value, 'method') as mockName but this makes my tests messy.

from some.module import SomeClass


class MockObject:
    def method(self):
        pass


class TestClass:
    @patch('SomeClass.open', return_value=MockObject())
    @patch('??????.method')  # I can't find any value to put here that works
    def test_as_desired(self, mockMethod, mockOpen):
        instance = SomeClass.open()
        instance.method()

        mockOpen.assert_called_once()
        mockMethod.assert_called_once()

    @patch('SomeClass.open', return_value=MockObject())
    def test_messy(self, mockOpen):
        with patch.object(mockOpen.return_value, 'method') as mockMethod:
            instance = SomeClass.open()
            instance.method()

            mockOpen.assert_called_once()
            mockMethod.assert_called_once()
  • Patching is for modifying objects whose creation is beyond your control; that's not the case for the instance of `MockObject`. – chepner Jun 11 '19 at 18:58
  • The reason I want to patch `MockObject.method` is to verify that the method is indeed called (and with the arguments I expected, which I left out of this example for simplicity) – amy_transcendent Jun 11 '19 at 19:13
  • So configure it when you *instantiate* it; don't try to patch it after the fact. – chepner Jun 11 '19 at 19:17
  • I'm instantiating it as the return value of another `@patch`. And again, the reason that I want to `@patch` the method on the mock object is so that I can inject it into the test function and ensure that it's called correctly. – amy_transcendent Jun 11 '19 at 19:21
  • I appreciate the comments and suggestions. There's a reason I'm trying to do what I'm trying to do, though. – amy_transcendent Jun 11 '19 at 19:22

1 Answers1

3

I think you are overcomplicating things:

import unittest.mock

@unittest.mock.patch.object(SomeClass, 'open', return_value=mock.Mock())
def test_as_desired(self, mock_open):
    instance = SomeClass.open()  # SomeClass.open is a mock, so its return value is too
    instance.method()

    mock_ppen.assert_called_once()
    instance.method.assert_called_once()
Blundell
  • 75,855
  • 30
  • 208
  • 233
chepner
  • 497,756
  • 71
  • 530
  • 681
  • That makes a lot of sense. Unfortunately your sample code doesn't work in pytest, because the @patch.object call doesn't seem to understand kwargs the way you're using them. I substituted `@patch.object(SomeClass, 'open', return_value=Mock())` but then the assertion `instance.method.assert_called_once()` fails – amy_transcendent Jun 11 '19 at 19:32
  • Actually, what I said is not quite correct: once I correct the `@patch` call, I can then assert that `mock_open.return_value.method.assert_called_once` – amy_transcendent Jun 11 '19 at 19:37