TL;DR
Can I access the current unittest.TestCase
instance from within a method of a mock class without explicitly passing in that instance? If not, what is the proper way to access the assertion helper functions of TestCase
(which are instance methods) from there? (Is there a proper way?)
The Szenario: Augmenting MagicMock
with my own assertion method that builds upon a TestCase
method
I'd like to test that one function (handle_multiple_foobars()
) uses another function (handle_one_foobar()
) correctly, so I'm mocking handle_one_foobar()
. 'Correctly' here means that handle_multiple_foobars()
shall call handle_one_foobar()
for each of its arguments individually. (One handle_one_foobar()
call per handle_multiple_foobars()
argument.) I don't care about the order of the calls.
Checking whether all expected calls to the mock have been made
Thus, I've started with this:
import unittest
from unittest import TestCase
from unittest.mock import patch, call
def handle_one_foobar(foobar):
raise NotImplementedError()
def handle_multiple_foobars(foobars):
for foobar in reversed(foobars):
handle_one_foobar(foobar)
class FoobarHandlingTest(TestCase):
@patch('__main__.handle_one_foobar')
def test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar(
self, handle_one_foobars_mock):
foobars = ['foo', 'bar', 'foo', 'baz']
handle_multiple_foobars(foobars)
expected_calls = [call(fb) for fb in foobars]
handle_one_foobars_mock.assert_has_calls(
expected_calls,
any_order=True)
if __name__ == '__main__':
unittest.main()
Checking whether only expected calls to the mock have been made
However, this would also pass if there were more calls to the mocked function, e.g.
def handle_multiple_foobars(foobars):
handle_one_foobar('begin')
for foobar in reversed(foobars):
handle_one_foobar(foobar)
handle_one_foobar('end')
I don't want that.
I could easily write an additional assertion (or an additional test) to check for the total count of calls. But I'd like this to be treated as a single condition to test. So I've constructed a different assertion:
class FoobarHandlingTest(TestCase):
@patch('__main__.handle_one_foobar')
def test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar(
self, handle_one_foobar_mock):
foobars = ['foo', 'bar', 'foo', 'baz']
handle_multiple_foobars(foobars)
expected_calls = [call(fb) for fb in foobars]
self.assertCountEqual(
handle_one_foobar_mock.mock_calls,
expected_calls)
This would catch the additional calls nicely:
F
======================================================================
FAIL: test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar (__main__.FoobarHandlingTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/lib/python3.4/unittest/mock.py", line 1136, in patched
return func(*args, **keywargs)
File "convenientmock.py", line 23, in test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar
expected_calls)
AssertionError: Element counts were not equal:
First has 1, Second has 0: call('begin')
First has 1, Second has 0: call('end')
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Move the new assertion into a helper method of the mock
But for my taste, it's not obvious enough what's being asserted here. Therefore, I decided to extract the assertion into a new function. Because this function serves a very similar purpose as assert_has_calls()
, I feel it should be a method of the mock class. It's not hard to extend MagicMock
and we can even make the extracted method a bit more generic in allowing to specify whether call order should matter or not:
from unittest.mock import MagicMock
class MyMock(MagicMock):
def assert_has_exactly_calls(_mock_self, calls, any_order=False):
tc = TestCase()
asserter = tc.assertCountEqual if any_order else tc.assertEqual
asserter(_mock_self.mock_calls, list(calls))
@patch
will use this class instead of unittest.mock.MagicMock
to create the mock when I change the test method decoration to
@patch('__main__.handle_one_foobar', new_callable=MyMock)
def test_handle_multiple_foobars_calls_handle_one_foobar_for_each_foobar( # ...
I can then write my assertion as
handle_one_foobar_mock.assert_has_exactly_calls(
expected_calls,
any_order=True)
... and the ugly
But you might have noticed something very ugly: To be able to use TestCase
instance methods assertCountEqual()
and assertEqual()
, I've created a dummy TestCase
instance that isn't the real FoobarHandlingTest
instance in which the test runs.
How can I avoid this?
Obviously, I could pass the test's self
into the assertion method, but that'd make for a very non-intuitive signature. (Why should I have to tell an assertion about my test case?)