Single assert / Single mock rule in practice

Mateusz Łoskot <[email protected]> Tue, 14 Jan 2014 11:37:00 +0000
Newsgroups gmane.comp.programming.test-driven-development
Message-ID <CABUeae_1GJLUTGfSxt-oRiMv-B7gvPGJqmK8PX9tvEOQmQaOzQ@mail.gmail.com>
Hi,

I'd like to ask for ideas of best practices in case of developing and testing
a wrapper for a Python legacy module.
Especially, about applying the single assert / single mock rule
which I have been learning about from numerous books/videos/blogs.

I have Python xdep module which exposes API in form of bunch of free functions.
I'm creating new Python module foo which uses xdep (the idea is to provide
simplified, Pythonic interface, but that is not directly relevant to
my question here).

I'm trying to keep the code as simple & generic as possible, so my question is
not about Python but unit testing and mocking.

# xdep.py
def open():
   ...
def get_item(item_id):
   ...
def close():
   ...

# foo.py
import xdep

def exists(item_id):
    xdep.open()
    item = xdep.get_item(item_id)
    xdep.close()
    if item:
        return item.id == item_id
    else:
        return false

Here is my module to unit test behaviuour of the foo:
- I create and plug a stub for the legacy module xdep
- I mock functions of xdep

# test_foo.py
import unittest
import unittest.mock as mock

class ItemTest(unittest.TestCase):
    def setUp(self):

        # create stub for external dependency of foo
        self.xdep_stub = mock.Mock()
        patcher = mock.patch.dict('sys.modules', {'xdep': self.xdep_stub})
        self.addCleanup(patcher.stop)
        patcher.start()

        # import foo does import xdep, as patched above
        import foo
        self.foo = foo

    def test_exists_ExistingItemId_ReturnTrue(self):
        foo = self.foo
        # Arrange
        ### Data
        item_id = 1
        ### Mock
        foo.xdep.get_item.return_value = 1
        # Act
        item_exists = foo.item.exists(item_id)
        # Assert
        self.assertTrue(item_exists)


I have a single assertion and I use single mock which mocks xdep.get_item
function, so it seems a valid yet clear unit test.

Now, having the single assert/single mock rule in mind, I have some questions,
about the test_exists_ExistingItemId_ReturnTrue in particular:

1) How asserting on foo.item.exists return value is different from
asserting how xdep.get_item mock was called?

I could have replaced
self.assertTrue(item_exists)
with
foo.xdep.get_item.assert_called_once_with(1)

2) What about having both assertions, would that break the single
assertion rule?

self.assertTrue(item_exists)
foo.xdep.get_item.assert_called_once_with(1)

I'm not concern about breaking the rule as technical convention, but as:
am I testing multiple things here?

3) AFAIU, it's natural that unit tests are coupled with implementation of
method/behaviour they are testing, foo.exists function in my case.

So, given that foo.exists function calls three functions of the legacy
xdep module
xdep.open
xdep.get_item
xdep.close
should I mock all the three function and verify how they were called?
Would that still belong to scope of test_exists_ExistingItemId_ReturnTrue test
or I better create a new dedicated test case(s), something like
test_exists_IfCalled_xdepOpenCalled
test_exists_IfCalled_xdepCloseCalled
test_exists_IfCalled_xdepGetItemCalled
and do assert_called_once_with() appropriately.

Above, I count each mock for each xdep function as a separate mock,
so "single mock per test" rule seems to suggests I should verify they are called
in separate tests, doesn't it?

Also, some of other xdep functions need to be called with multiple parameters,
and as setting up and verifying their mocks may be quite elaborate,my
gut feeling
tells me that single unit test per "legacy API call expectation" as listed above
is a good approach for readability and maintainability of tests.
Am I right?

4) What other, if any, approach to single assert/single mock rules
would you take for testing similar case as above?


Best regards,
-- 
Mateusz  Łoskot, http://mateusz.loskot.net