1

I have two classes A and B. I want to test function foo_a of A. Using some business logic, foo_a creates two possible instances of B (with different arguments) and calls function foo_b of B:

class A:
  def foo_a(self, *args, **kwargs):
    # some code above
    if condition_1:
      foo_b_result = B(**arguments_1).foo_b()
    else:
      foo_b_result = B(**arguments_2).foo_b()
    # some code below that returns foo_b_result + other objects independent of foo_b_result

The problem comes from the fact that foo_b is a complexe function that calls APIs and other stuff. I don't want to mock APIs here in this unit test so that foo_b returns the appropriate output, indeed, foo_b is already tested elsewhere in my set of unit tests for class B. Thus, in this context I care more about the correctness of arguments_1 and arguments_2 than the correctness of foo_b_result. Therefore, It would be nice if I could mock foo_b so that it returns outputs that could easily show if foo_b has been called with the right instance of B. For instance, foo_b could return arguments_1 or arguments_2.

Maybe there is another better approach for unit testing foo_a in this case, any idea is welcome.

UPDATE: Another way would be to create a new function that takes as input condition_1 and returns either arguments_1 or arguments_2 (this function could be tested easily). Then I would mock foo_b so that it always return the same value (for instance 0). But still, it would be interesting to know if it's possible to mock foo_b so that it returns values according to arguments_1 or arguments_2.

dallonsi
  • 1,299
  • 1
  • 8
  • 29
  • What do you mean you want to mock `foo`, specifically? Do you mean you want to replace a `MyClass` instance with a test double? – jonrsharpe Jun 03 '21 at 15:41
  • just edited the question. Clearer ? – dallonsi Jun 03 '21 at 15:46
  • Not really. If you're testing `MyClass` you shouldn't be mocking any part of it. If you're testing something that _uses_ a `MyClass` why not pass it e.g. `MyClass(a=0)`, whose `foo` method would return `0` as desired? – jonrsharpe Jun 03 '21 at 15:47
  • Could you give a more representative one, then? Are you testing some other part of `MyClass` that _calls_ `self.foo()`, for example? And I'm not sure why you think that's untrue, trying to replace parts of the thing you're supposed to be testing with test doubles makes it much more difficult to e.g. confidently refactor within the class and couples your tests closely to the current implementation. – jonrsharpe Jun 03 '21 at 15:55
  • alright, I'll make my question more precise then – dallonsi Jun 03 '21 at 15:58
  • OK, thank you. In the meantime here are some concrete examples (albeit in JS) of why I'd generally recommend not mocking within the class: https://stackoverflow.com/a/66752334/3001761. – jonrsharpe Jun 03 '21 at 16:01
  • Hi @jonrsharpe I rewrote the question in a more detailed way. Thanks for your help and the link to your post – dallonsi Jun 04 '21 at 07:43
  • 1
    _"show if `foo_b` has been called with the right instance of `B`"_ - no, you need to show if it's been called _on_ the right instance of `B`, which really just means what `B` was called with. You can see how to mock a class at e.g. https://stackoverflow.com/a/38199345/3001761, then you can assert on what the mock of `B` was called with. You shouldn't need to put real behaviour (_"it returns values according to `arguments_1` or `arguments_2`"_) into the test double for a case like this. – jonrsharpe Jun 04 '21 at 08:22
  • Oh ok I see. "Then you can assert on what the mock of B was called with" can you develop a bit ? How can I do this ? – dallonsi Jun 04 '21 at 12:55
  • 1
    Mocks have a bunch of methods for asserting on interactions: https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called – jonrsharpe Jun 04 '21 at 12:58
  • ok I know how check if my Mock's function `foo` was called with some attributes like so: `mock = Mock(MyClass) mock.foo(b=3) mock.foo.assert_called_with(b=3)` but this does not seem to work when I replace `foo` by `__init__`. Thanks for helping – dallonsi Jun 04 '21 at 13:15
  • alriiight `mock = Mock(MyClass) mock(b=3) mock.assert_called_with(b=3)` is what I needed... Thanks – dallonsi Jun 04 '21 at 13:17
  • do you want to make an answer with all the ideas above ? Otherwise I'll do it – dallonsi Jun 04 '21 at 13:18

1 Answers1

0

Thanks to @jonrsharpe I ended up with the following solution:

from unittest import mock
from unittest.mock import MagicMock

@mock.patch('a.B')  # a.py contains class A with an import of class B
def test_foo_a(mock_b):  # mock_b mocks class B
  return_value_foo_b = "whatever"
  mock_b().foo_b = MagicMock(return_value=return_value_foo_b)
  # instantiate A in a variable called a, then:
  expected_foo_a = (return_value_foo_b,) # + other objects independent of foo_b_result
  assert a.foo_a() == expected_foo_a
  mock_b.assert_called_with(arguments_1)  # condition_1 is met.
dallonsi
  • 1,299
  • 1
  • 8
  • 29