Home > Net >  How to use @patch.object decorator for a method that is being called in multiple places in a class?
How to use @patch.object decorator for a method that is being called in multiple places in a class?

Time:10-12

I'm having an implementation class where, there's this save method which is being called in multiple places within the class.

So basically that method intakes an argument and returns a file url which is a string.

In the class I'm trying to test, I'm saving multiple files in different locations. Hence how can I test that in my UnitTest class?

For eg I was able to mock the delete method like below, which is being called only once:

@patch.object(FileStoreSp, "delete_file", return_value=True)

But for the save method I'm not sure how can i test it since its being called in multipe places and it returns different values. Is there a way I can pass the return values in some sort of an order in which the method is being called?

Any help could be appreciated.

CodePudding user response:

You could monkey patch the save method. You could create a temp directory and test that everything is in place after your function has run.

However, the scenario, which you describe, indicates that you probably should refactor your code to be more testable. Writing files is a so called "side-effect". Side-effects make your test harder (maybe impossible) to test. Try to avoid side-effects, if possible. And if they are really needed, then try to concentrate side effects in one place at the boundary of your system. There are many strategies to archive this. For example:

  1. Rearrange function calls
  2. Delegate the execution of the side effect. E.g. let the function return a value of what should be done (return "write-file", "Filename") and handle those at the top level

If you really cannot change the code (maybe its 3rd party code out of your control), then you can monkey patch nearly everything in python. How to do it best depends on your concrete scenario and code. For the unittest framework have a look at MagicMock.

CodePudding user response:

If I understand correctly, you have some method on your class and you want to test that method. And that method calls another method (save) more than once. Now you want to mock out the save method, while testing that other method, which is the correct approach.

Let's abstract this for a moment. Say the method you are testing is called bar and inside it calls the method foo twice. Now foo does all sorts of stuff including side effects (disk I/O, whatever), so you obviously want to mock it during the bar test. Yet you want to ensure that foo is called in the way you expect it from bar and also that bar does something specific with the return values it gets from foo.

Thankfully, the Mock class allows you to set the side_effect attribute in various ways. One of them is setting it to an iterable. Calling the mock once then returns the next element from that iterable. This allows you to set multiple distinct return values for the mocked object in advance.

We can then leverage the assert_has_calls method of the mocked object using call objects to verify that foo was called with the expected arguments.

Here is an example to illustrate the concept:

from unittest import TestCase
from unittest.mock import MagicMock, call, patch


class MyClass:
    def foo(self, string: str) -> list[str]:
        print("Some side effect")
        return string.split()

    def bar(self, string1: str, string2: str) -> tuple[str, str]:
        x = self.foo(string1)[0]
        y = self.foo(string2)[0]
        return x, y


class MyTestCase(TestCase):
    @patch.object(MyClass, "foo")
    def test_bar(self, mock_foo: MagicMock) -> None:
        # Have mocked `foo` return ["a"] first, then ["b"]
        mock_foo.side_effect = ["a"], ["b"]
        # Thus, we expect `bar` to return ("a", "b")
        expected_bar_output = "a", "b"

        obj = MyClass()
        # The arguments for `bar` are not important here,
        # they just need to be unique to ensure correct calls of `foo`:
        arg1, arg2 = MagicMock(), MagicMock()
        output = obj.bar(arg1, arg2)

        # Ensure the output is as expected:
        self.assertTupleEqual(expected_bar_output, output)
        # Ensure `foo` was called as expected:
        mock_foo.assert_has_calls([call(arg1), call(arg2)])

Hope this helps.

  • Related