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?

0

1 Answer 1

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'>)]
    
Sign up to request clarification or add additional context in comments.

11 Comments

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: if your .c() result is really a custom object emulates a numeric type, 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.
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: so above, the 'something' is whatever mock_c.return_value is set to.
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.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.