Search code examples
pythonpython-unittest

What is the difference between patching a Python class and the method of a python class?


I believe the correct way to patch is to mock the Python class then call .return_value on the patched class but I'm not sure why. Say I have some code:

class.py:

class SomeClass:
  def __init__(self, some_value):
    self.some_value = some_value

  def add_to_value(self, add_value):
    self.some_value = self.some_value + add_value

function_thing.py:

from my_pkg.class import SomeClass

def some_function():
    some_class_instance = SomeClass(5)
    some_class_instance.add_to_value(6)

test_function_thing.py:

from my_pkg.function_thing import some_function
class TestSomeFunction(TestCase)
  # This works, and it knows the function was called with 6
  @patch("my_pkg.function_thing.SomeClass")
  def test_some_function_01(self, mock_some_class):
    instance_mock = mock_some_class.return_value
    some_function()
    instance_mock.add_to_value.assert_called_with(6)

  # This does not work.
  @patch("my_pkg.function_thing.SomeClass.add_to_value")
  def test_some_function_02(self, mock_add_to_value):
    some_function()
    # This succeeds.
    mock_add_to_value.assert_called_once()
    # This fails, saying it wasn't called with 6 but some object
    mock_add_to_value.assert_called_with(6)

It appears that, in order for the mock to correctly understand how the class was called, we can't mock the function as a whole. But we can assert that the function was called. I would think that either asserting the function was called would fail along said how it was called, or both would succeed. Thanks for any info!


Solution

  • That's because SomeClass.add_to_value takes two arguments: self andadd_value.

    def add_to_value(self, add_value):
    

    Even though Python syntax lets you magic away the self argument when calling, it's still there as far as Python is concerned. So you've asserted the method is called with argument tuple (6,) when really it's called with argument tuple (whatever, 6), where whatever is the instance produced by some_function.

    Now, we could keep mocking, as you did in your example. But we should probably stop. If you find that you need to mock half of your codebase to test one function, that's a good indicator that that function is making a lot of assumptions and should be decoupled from everything else. In this case, you could pass a SomeClass instance to some_function

    def some_function(some_class_instance=None):
        if some_class_instance is None:
            some_class_instance = SomeClass(5)
        some_class_instance.add_to_value(6)
    

    Now existing callers still get the "nice" default, but tests (and other users) can call the function with whatever instance they choose.

    You could also inject at the factory level, passing a factory for a class.

    def some_function(class_to_instantiate=SomeClass):
        some_class_instance = class_to_instantiate(5)
        some_class_instance.add_to_value(6)
    

    Now you can change the class_to_instantiate argument to a class you have full control over in order to test it.