Adding Mock Objects In Python
Solution 1:
Adding objects requires those objects to at least implement __add__
, a special method, called magic methods by Mock, see the Mocking Magic Methods section in the documentation:
Because magic methods are looked up differently from normal methods, this support has been specially implemented. This means that only specific magic methods are supported. The supported list includes almost all of them. If there are any missing that you need please let us know.
The easiest way to get access to those magic methods that are supported by mock
, you can create an instance of the MagicMock
class, which provides default implementations for those (each returning a now MagicMock
instance by default).
This gives you access to the x.a + x.b
call:
>>> from unittest import mock
>>> m = mock.MagicMock()
>>> m.a + m.b
<MagicMock name='mock.a.__add__()'id='4500141448'>
>>> m.mock_calls
[call.a.__add__(<MagicMock name='mock.b'id='4500112160'>)]
A call to m.a.__add__()
has been recorded, with the argument being m.b
; this is something we can now assert in a test!
Next, that same m.a.__add__()
mock is then used to supply the .c()
mock:
>>> (m.a + m.b).c()
<MagicMock name='mock.a.__add__().c()'id='4500162544'>
Again, this is something we can assert. Note that if you repeat this call, you'll find that mocks are singletons; when accessing attributes or calling a mock, more mocks of the same type are created and stored, you can later use these stored objects to assert that the right object has been handed out; you can reach the result of a call with the Mock.return_value
attribute:
>>> m.a.__add__.return_value.c.return_value
<MagicMock name='mock.a.__add__().c()'id='4500162544'>
>>> (m.a + m.b).c() is m.a.__add__.return_value.c.return_value
True
Now, on to round()
. round()
calls a magic method too, the __round__()
method. Unfortunately, this is not on the list of supported methods:
>>> round(mock.MagicMock())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type MagicMock doesn't define __round__ method
This is probably an oversight, since other numeric methods such as __trunc__
and __ceil__
are included. I filed a bug report to request it to be added. You can manually add this to the MagicMock
supported methods list with:
mock._magics.add('__round__') # set of magic methods MagicMock supports
_magics
is a set; adding __round__
when it already exists in that set is harmless, so the above is future-proof. An alternative work-around is to mock the round()
built-in function, using mock.patch()
to set a new round
global in the module where your function-under-test is located.
Next, when testing, you have 3 options:
Drive tests by setting return values for calls, including types other than mocks. For example, you can set up your mock to return a floating point value for the
.c()
call, so you can assert that you get correctly rounded results:>>> m.a.__add__.return_value.c.return_value = 42.12# (m.a + ??).c() returns 42.12 >>> round((m.a + m.b).c()) == 42True
Assert that specific calls have taken place. There are a whole series of
assert_call*
methods that help you with testing for a call, all calls, calls in a specific order, etc. There are also attributes such as.called
,.call_count
, andmock_calls
. Do check those out.Asserting that
m.a + m.b
took place means asserting thatm.a.__add__
was called withm.b
as an argument:>>> m = mock.MagicMock() >>> m.a + m.b <MagicMock name='mock.a.__add__()'id='4500337776'> >>> m.a.__add__.assert_called_with(m.b) # returns None, so success
If you want to test a
Mock
instance return value, traverse to the expected mock object, and useis
to test for identity:>>>mock._magics.add('__round__')>>>m = mock.MagicMock()>>>r = round((m.a + m.b).c())>>>mock_c_result = m.a.__add__.return_value.c.return_value>>>r is mock_c_result.__round__.return_value True
There is never a need to go back from a mock result to parents, etc. Just traverse the other way.
The reason your lambda for __add__
doesn't work is because you created a Mock()
instance with arguments. The first two arguments are the spec
and the side_effect
arguments. The spec
argument limits what attributes a mock supports, and since you passed in a
as a mock object specification and that a
object has no attribute c
, you get an attribute error on c
.
Solution 2:
I found a solution myself, but it isn't all too beautiful. Bear with me.
The normal Mock
objects are prepared to record a lot of treatment they experience but not all. E. g. they will record when they are being called, when there is an attribute being queried, and some more things. They will not, however, record (or accept) if they are e. g. added to each other. Adding is assumed to be a "magic" operation, using a "magic method" (__add__
) of the objects and Mock
s don't support them.
For these there is another class called MagicMock
. MagicMock
objects support the magic methods, so adding them works for them. The result will be another MagicMock
object which can be asked how it was created (by adding two other MagicMock
objects).
Unfortunately, in the current version (3.6.5) the magic method __round__
(which is called when round(o)
is called) is not included yet. My guess is they just forgot to list that among the other magic methods like __trunc__
, __floor__
, __ceil__
, etc. When I added it in the sources I could properly test also my code under test including the round()
call.
But patching the installed Python modules is not the way to do it of course. Since it is a flaw in the current implementation which I expect will be fixed in the future, my current solution is to only change the internal data structures of the mock
module after importing it.
The way my test now look is this:
defto_be_tested(x):
returnround((x.a + x.b).c())
import unittest
import unittest.mock
# patch mock module's internal data structures to support round():
unittest.mock._all_magics.add('__round__')
unittest.mock._magics.add('__round__')
classTest_X(unittest.TestCase):
deftest_x(self):
m = unittest.mock.MagicMock()
r = to_be_tested(m)
# now for the tests:
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)
Test_X().test_x()
A simple test like self.assertEqual(r, round((m.a + m.b).c()))
sadly isn't enough because that does not check the name of the attribute b
(and who knows what else).
Post a Comment for "Adding Mock Objects In Python"