0

I have this code under test:

def to_be_tested(x):
  return round((x.a + x.b).c())

In my unittest I want to assert that exactly this is done with the passed x and the result returned, so I pass a MagicMock object as x:

class Test_X(unittest.TestCase):
  def test_x(self):
    m = unittest.mock.MagicMock()
    r = to_be_tested(m)

Then I check the result for being what I expect:

    self.assertEqual(r._mock_new_name, '()')  # created by calling
    round_call = r._mock_new_parent
    self.assertEqual(round_call._mock_new_name, '__round__')
    c_result = round_call._mock_new_parent
    self.assertEqual(c_result._mock_new_name, '()')  # created by calling
    c_call = c_result._mock_new_parent
    self.assertEqual(c_call._mock_new_name, 'c')
    add_result = c_call._mock_new_parent
    self.assertEqual(add_result._mock_new_name, '()')  # created by calling
    add_call = add_result._mock_new_parent
    self.assertEqual(add_call._mock_new_name, '__add__')
    a_attribute = add_call._mock_new_parent
    b_attribute = add_call.call_args[0][0]
    self.assertEqual(a_attribute._mock_new_name, 'a')
    self.assertEqual(b_attribute._mock_new_name, 'b')
    self.assertIs(a_attribute._mock_new_parent, m)
    self.assertIs(b_attribute._mock_new_parent, m)

After importing unittest.mock I need to patch the internal structure of the mock module in order to be able to properly magic-mock the round() function (see https://stackoverflow.com/a/50329607/1281485 for details on that):

unittest.mock._all_magics.add('__round__')
unittest.mock._magics.add('__round__')

So, now, as I said, this works. But I find it extremely unreadable. Furthermore I needed to play around a lot to find the things like _mock_new_parent etc. The underscore also indicates that this is a "private" attribute and shouldn't be used. The documentation doesn't mention it. It also does not mention another way of achieving what I try to.

Is there a nicer way to test returned MagicMock objects for being created the way they should have been?

Alfe
  • 56,346
  • 20
  • 107
  • 159

1 Answers1

1

You are going overboard. You are testing the implementation, not the result. Moreover, you are reaching into internals of the mock implementation that you do not need to touch.

Test that you get the right result, and test that the result is based on the inputs you want to be used. You can set up the mock such that round() is passed an actual numeric value to round:

  • x.a + x.b results in a call to m.a.__add__, passing in m.b.
  • m.a.__add__().c() is called, so we can test that it was called if that's needed.
  • Just set the result of c() to a number for round() to round off. Getting the correct round(number) result from the function means .c() was called.

Passing in a number to round() is sufficient here, because you are not testing the round() function. You can rely on the Python maintainers to test that function, focus on testing your own code.

This is what I'd test:

m = unittest.mock.MagicMock()

# set a return value for (x.a + *something*).c()
mock_c = m.a.__add__.return_value.c
mock_c.return_value = 42.4

r = to_be_tested(m)

mock_c.assert_called_once()
self.assertEqual(r, 42)

If you must assert that m.a + m.b took place, then you can add

m.a.__add__.assert_called_once(m.b)

but the mock_c call assert passing is already proof that at least a (m.a + <whatever>) expression took place and that c was accessed on the result.

If you must validate that round() was used on an actual mock instance, you'll have to stick to patching the MagicMock class to include __round__ as a special method and remove the mock_c.return_value assignment, after which you can assert that the return value is the correct object with

# assert that the result of the `.c()` call has been passed to the
# round() function (which returns the result of `.__round__()`).
self.assertIs(r, mock_c.return_value.__round__.return_value)

Some further notes:

  • There is no point in trying to make everything a mock object. If the code under test is supposed to work on standard Python types, just have your mocks produce those types. E.g. if some call is expected to produce a string, have your mock return a test string, especially when you are then passing stuff to other standard-library APIs.
  • Mocks are singletons. You do not need to work back from a given mock to test that they have the right parent, because you can reach the same object by traversing the parent attributes and then use is. E.g. if a function returns the a mock object somewhere, you can assert that the right mock object was returned by testing assertIs(mock_object.some.access.return_value.path, returned_object).
  • When a mock is called, that fact is recorded. You can assert this with the assert_called* methods, the .called and .call_count attributes, and traverse the result of calls with the .return_value attributes
  • When in doubt, inspect the .mock_calls attribute to see what the code-under-test has accessed. Or do so in an interactive session. For example, it's easier to see what m.a + m.b does in a quick test with:

    >>> from unittest import mock
    >>> m = mock.MagicMock()
    >>> m.a + m.b
    <MagicMock name='mock.a.__add__()' id='4495452648'>
    >>> m.mock_calls
    [call.a.__add__(<MagicMock name='mock.b' id='4495427568'>)]
    
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Right, but your proposition is that it doesn't make sense to test the implementation. That's not true in all cases. If you happen to have to refactor some code which does things nobody clearly understands (anymore) but which must not change due to the refactoring, just testing that the things done effectively (without understanding why) might make sense. I'd love to be able to use the `MagicMocks` in this case. Of course, in normal situations it makes perfect sense to understand what the code does and test it on a higher level (check the meaning of the results). – Alfe May 14 '18 at 15:59
  • @Alfe: if your `.c()` result is really a custom object [emulates a numeric type](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types), you'd have separate tests for the numeric type hooks. I'd still stick to the above test; this is not the place to test if `round()` will work on your custom type. – Martijn Pieters May 14 '18 at 16:28
  • Actually I only want to make sure that it gets *called* with my "something" as parameter (and don't want to figure out what that something is, actually, just to be able to write the test case). Sure, testing `round()` is not the issue here. – Alfe May 14 '18 at 16:31
  • @Alfe: so above, the 'something' is whatever `mock_c.return_value` is set to. – Martijn Pieters May 14 '18 at 16:42
  • I want e. g. to have a unittest which shows that a refactorer replaced the call of `round()` by `trunc(x+0.5)` or similar. Of course, these examples here make only limited sense. I think in my actual (not so mcve-like) case this would be a useful use of a `MagicMock`. – Alfe May 14 '18 at 16:57
  • @Alfe: Why? You are then focusing on if the implementation is correct, not if the function produces the correct results. If the function still returns `42`, *it is correct and working, doing it's job*. Tests should not be in the business of verifying if someone used the correct approved method of implementation to get a job done. – Martijn Pieters May 14 '18 at 17:03
  • @Alfe: Now, it could be that the refactor to use `trunc()` breaks the test because `x + 0.5` returns an object that `trunc()` doesn't support, or whose implementation of the `__trunc__` method is broken. Using a MagicMock object *would not detect that problem at all*. Your test would break (the returned object is `mock_c.return_value.__trunc__.return_value`), you fix your test to use the `__trunc__` version, but the actual production system still breaks because no test puts the proper types through your function-under-test. – Martijn Pieters May 14 '18 at 17:07
  • @Alfe: so if your system is actually using custom classes that emulate numbers, you better use those types to test this function, at least as an integration test. Otherwise, just stick to returning an actual floating point number and testing that the function does its job. – Martijn Pieters May 14 '18 at 17:08
  • Mocks provide means to figure out whether the CUT *behaves* correctly instead of only whether it just *returns* the correct results. Mocking objects which are supposed to get called is such a thing. Sometimes the spec describes rather a *behavior* than just the *result*. One could always argue that it doesn't matter whether some object really gets called, as long as the results (and side effects) are in order. We do not only mock to *prevent* real calls but also to *detect* them. I just take this one step further and would also like to check that `a + b` etc. really has happened. – Alfe May 15 '18 at 08:42
  • @Alfe: `m.a.__add__.assert_called_once(m.b)` asserts that `a + b` really has happened. If you patch the library to include `__round__` as a magic method, you can validate the return value with `r is mock_c.return_value.__round__.return_value`. – Martijn Pieters May 15 '18 at 11:31
  • Well, `a + b` was just an minimal example in that comment. The question was, how can I easily assert that more complex things like `round((m.a + m.b).c())` really have happened (and that formula also was just an example; it could become more complex), ideally in a way so that the testing code is still readable. The way I'm using in my Q does not seem feasible, obviously. – Alfe May 15 '18 at 12:18