2

I'm using the mock Python module for performing my tests.

There are times when I'm mocking a class, however I just want to mock some of its methods and properties, and not all of them.

Suppose the following scenario:

# module.py
class SomeClass:
    def some_method(self):
        return 100

    def another_method(self):
        return 500

# test.py
class Tests(unittest.TestCase):
    @patch('module.SomeClass')
    def test_some_operation(self, some_class_mock):
        some_class_instance = some_class_mock.return_value

        # I'm mocking only the some_method method.
        some_class_instance.some_method.return_value = 25

        # This is ok, the specific method I mocked returns the value I wished.
        self.assertEquals(
            25,
            SomeClass().some_method()
        )

        # However, another_method, which I didn't mock, returns a MagicMock instance
        # instead of the original value 500
        self.assertEquals(
            500,
            SomeClass().another_method()
        )

On the code above, once I patch the SomeClass class, calls to methods whose return_values I didn't exlicitely set will return MagicMock objects.

My question is: How can I mock only some of a class methods but keep others intact?

There are two ways I can think of, but none of them are really good.

  1. One way is to set the mock's method to the original class method, like this:

    some_class_instance.another_method = SomeClass.another_method
    

    This is not really desirable because the class may have a lot of methods and properties to "unmock".

  2. Another way is to patch each method I want explicitly, such as:

     @patch('module.SomeClass.some_method')
     def test_some_operation(self, some_method_mock):
    

    But this doesn't really work if I want to mock the class itself, for mocking calls to the initializer for example. The code below would override all SomeClass's methods anyway.

     @patch('module.SomeClass.some_method')
     @patch('module.SomeClass')
     def test_some_operation(self, some_class_mock, some_method_mock):
    

Here is a more specific example:

class Order:
    def process_event(self, event, data):
        if event == 'event_a':
            return self.process_event_a(data)

        elif event == 'event_b':
            return self.process_event_b(data)

        else:
            return None

    def process_event_a(self, data):
        # do something with data

    def process_event_b(self, data):
        # do something different with data

In this case, I have a general method process_event which calls a specific processing event depending on the supplied event.

I would like to test only the method process_event. I just want to know if the proper specific event is called depending on the event I supply.

So, in my test case what I want to do is to mock just process_event_a and process_event_b, call the original process_event with specific parameters, and then assert either process_event_a or process_event_b were called with the proper parameters.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Felipe Ferri
  • 3,488
  • 2
  • 33
  • 48
  • You shouldn't mock parts of the thing you're supposed to be testing. If you find you want to *partially* mock something, that's a suggestion you need to rethink the approach. – jonrsharpe Oct 31 '20 at 19:03
  • You should only use mocks at the interface. If the thing you are testing needs an object to play with, and you want some of that objects things to be real and others to be false, your interface is probably too large. Simplify the attack surface so that it is clear where your mocks should be implemented. If it is unclear, think about your design some more. – Paul Becotte Oct 31 '20 at 19:07
  • Hey Jon and Paul, thanks for you responses. Ok, I understand you think I shouldn't do that... Does it mean there isn't a better way to do it than what I posted on the question? – Felipe Ferri Oct 31 '20 at 19:41
  • Your example is too abstract to say. Your test only invokes a mock, so it's not clear what you're actually trying to test. Use [test doubles](https://engineering.pivotal.io/post/the-test-double-rule-of-thumb/) for testing their *collaborators*, in this case you should be testing whatever *uses* `SomeClass`. – jonrsharpe Oct 31 '20 at 23:03
  • @jonrsharpe, thanks again. I added a more specific example on the question which I believe illustrates a case where partially mocking a class would make sense. – Felipe Ferri Nov 05 '20 at 15:42
  • That example demonstrates why it *doesn't*. The caller of `process_event` just wants their event to be processed; the fact that you've implemented that as calling through to different methods is an *implementation detail*. By testing that you've coupled your tests to the current implementation. – jonrsharpe Nov 05 '20 at 20:09
  • But shouldn't unit tests be coupled to the current implementation? Aren't unit tests suppposed to be "white box", where I use knowledge of the implementation in order to achieve full code coverage? – Felipe Ferri Nov 06 '20 at 12:19

1 Answers1

1

Instead of patching the whole class, you must patch the object. Namely, make an instance of your class, then, patch the methods of that instance.

Note that you can also use the decorator @patch.object instead of my approach.

class SomeClass:
    def some_method(self):
        return 100

    def another_method(self):
        return 500

In your test.py

from unittest import mock

class Tests(unittest.TestCase):
    

    def test_some_operation(self):
        some_class_instance = SomeClass()

        # I'm mocking only the some_method method.
        with mock.patch.object(some_class_instance, 'some_method', return_value=25) as cm:

            # This is gonna be ok
            self.assertEquals(
                25,
                SomeClass().some_method()
            )

            # The other methods work as they were supposed to.
            self.assertEquals(
                500,
                SomeClass().another_method()
            )
Felipe Ferri
  • 3,488
  • 2
  • 33
  • 48
Amir Afianian
  • 2,679
  • 4
  • 22
  • 46
  • Hello Amir! Thanks for pointing out the use of patch.object. I had to perform a small correction because, when used in the method implementation, it has to be used as a context manager. – Felipe Ferri Nov 05 '20 at 15:24