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.