Every mock.patch() is a little smell
The Python unittest.mock library (and the standalone mock package for Python 2) includes an extremely handy feature in patch(), which allows easy and safe monkey-patching for the purposes of testing.
You can use patch() as a context manager, class decorator or start and stop it yourself, but the most commonly used form (in my experience) is as a decorator, which monkey patches something for the duration of a single test function. This is typically used to stub out some subsystem to allow testing of a single component in isolation.
Say we wanted a test of an order processing system, then we might write it with patch() like so:
@patch('billing.charge_card') def test_card(charge_card): """Placing an order charges the credit card.""" orders.place_order(TEST_ORDER) charge_card.assert_called_with(TEST_ORDER_TOTAL, TEST_CREDIT_CARD)
Here, the test verifies that place_order() would call charge_card() with the right arguments - while ensuring that charge_card is not actually called in the test.
One of my earliest contributions to the Quartz project at Bank of America Merrill Lynch was to recommend and justify the inclusion of mock in the standard distribution of Python that is shipped to every machine (desktop and server) in the organisation. My rationale was that while dependency injection is a better principle, refactoring is a chicken-and-egg process - you need tests in place to support refactoring, and refactoring so that good tests are easy to write. mock.patch() was well-suited to wrap tests around my team's existing, poorly tested codebase, before refactoring, so that we could have tests in place to enable us to refactor.
Dependency Injection
Dependency Injection is a simple technique where you pass in at runtime the key objects a function or class might need, rather than constructing those objects within the code. For example, we could have structured the previous example for dependency injection and passed in (injected) a mock dependency:
def test_card(): """Placing an order charges the credit card.""" p = Mock() orders.place_order(TEST_ORDER, card_processor=p) p.charge_card.assert_called_with(TEST_ORDER_TOTAL, TEST_CREDIT_CARD)
Dependency injection sounds like (and arises from) a strictly object-oriented and Java-ish technique, but it is absolutely a valuable approach in Python too. In Python we generally don't suffer the pain of having to define concrete subclasses of abstract interfaces; we can simply duck-type. But the benefits of dependency injection are great: as we can pass mock objects in testing, we can pass different concrete objects to be used in production, which makes for much more flexible, ravioli-style code. For example, maybe we could define a DeferredCardProcessor to satisfy a slightly different use case later. The possibilities are opened up.
Overuse of patch()
The value of patch() is that is makes tests easier to write. But it doesn't add any extra value to you application. Dependency injection is perhaps a little more expensive at first but adds extra value later, when your code is genuinely better structured.
In the past few days I've seen a couple of examples (via Code Review) of code that has numerous nested patch decorators:
@patch('my.project.trading.getEnrichedTradeData', lambda: TEST_TRADE_DATA) @patch('my.project.subledger.getSubledgerData', getTestSubledgerData) @patch('my.project.metadata.getSubledgerCodes', return_value=TEST_CODES) @patch('my.project.reports.storeReport') def test(self, storeReport): buildReport() storeReport.assert_called_with(EXPECTED_REPORT)
The lack of useful dependency injection aside, I think this is rather hard to read. And if we look at the code for buildReport() and see it does something like
def buildReport(): trades = getEnrichedTradeData() subledger = getSubledgerData() codes = getSubledgerCodes() report = Report(trades, subledger, codes) storeReport(report)
then we've fallen into a trap of testing that the code does what the code says it does, rather than testing functional behaviour we care about.
Seeing (or writing) code like this should cause you to think that perhaps the code wasn't structured right. To avoid failling into this trap, think of every mock.patch() as a small code smell - a missed opportunity for dependency injection.
If you're struggling to refactor your code to build dependency injection in, a good way to start is to do a CRC Cards session, which will typically produce a design that includes opportunity for dependency injection, especially if you think about testability as one of your design criteria.
Comments
Comments powered by Disqus