When our code interacts with other classes through methods and attributes, these are referred to as collaborators. Mocking out collaborators using Voidspace Mock (http://www.voidspace.org.uk/python/mock, created by Michael Foord) provides a key tool for BDD. Mocks provide a way for provided canned behavior compared to stubs, which provide canned state. While mocks by themselves don't define BDD, their usage keenly overlaps the ideas of BDD.
To further demonstrate the behavioral nature of the tests, we will also use the spec nose plugin found in the Pinocchio project (http://darcs.idyll.org/~t/projects/pinocchio/doc).
As stated on the project's website, Voidspace Mock is experimental. This book was written using version 0.7.0 beta 3. There is the risk that more API changes will occur before reaching a stable 1.0 version. Given this project's high quality, excellent documentation, and many articles in the blogosphere, I strongly feel it deserves a place in this book.
For this recipe, we will be using the shopping cart application shown at the beginning of this chapter with some slight modifications.
recipe30_cart.py
and copy all the code from cart.py
created in the introduction of this chapter.__init__
to add an extra storer
attribute used for persistence.class ShoppingCart(object): def __init__(self, storer=None): self.items = [] self.storer = storer
store
method that uses the storer
to save the cart.def store(self): return self.storer.store_cart(self)
retrieve
method that updates the internal items
by using the storer
.def restore(self, id): self.items = self.storer.retrieve_cart(id).items return self
We need to activate our virtual environment and then install Voidspace Mock for this recipe.
voidspace
mock
by typing pip
install
mock
.pinocchio
by typing pip install
http://darcs.idyll.org/~t/projects/pinocchio-latest.tar.gz.pinocchio
raises some warnings. To prevent them, we also need to install figleaf
by typing pip install figleaf
.With the following steps, we will explore how to use mock to write a testable story:
recipe30_cart.py
, create a DataAccess
class with empty methods for storing and retrieving shopping carts.class DataAccess(object): def store_cart(self, cart): pass def retrieve_cart(self, id): pass
recipe30.py
to write test code.DataAccess
.import unittest from copy import deepcopy from recipe30_cart import * from mock import Mock class CartThatWeWillSaveAndRestoreUsingVoidspaceMock(unittest.TestCase): def test_fill_up_a_cart_then_save_it_and_restore_it(self): # Create an empty shopping cart cart = ShoppingCart(DataAccess()) # Add a couple of items cart.add("carton of milk", 2.50) cart.add("frozen pizza", 3.00) self.assertEquals(2, len(cart)) # Create a clone of the cart for mocking # purposes. original_cart = deepcopy(cart) # Save the cart at this point in time into a database # using a mock cart.storer.store_cart = Mock() cart.storer.store_cart.return_value = 1 cart.storer.retrieve_cart = Mock() cart.storer.retrieve_cart.return_value = original_cart id = cart.store() self.assertEquals(1, id) # Add more items to cart cart.add("cookie dough", 1.75) cart.add("ginger ale", 3.25) self.assertEquals(4, len(cart)) # Restore the cart to the last point in time cart.restore(id) self.assertEquals(2, len(cart)) cart.storer.store_cart.assert_called_with(cart) cart.storer.retrieve_cart.assert_called_with(1)
nosetests
with the spec
plugin.Mocks are test doubles that confirm method calls, which is the 'behavior'. This is different from stubs, which provide canned data, allowing us to confirm state.
Many mocking libraries are based on the record/replay pattern. They first require the test case to record every behavior the mock will be subjected to when used. Then we plug the mock into our code, allowing our code to invoke calls against it. Finally, we execute replay, and the Mock library compares the method calls we expected with the ones that actually happened.
A common issue with record/replay mocking is that, if we miss a single method call, our test fails. Capturing all the method calls can become very challenging when trying to mock out third-party systems, or dealing with variable calls that may be tied to complex system states.
The Voidspace Mock library differs by using the action/assert pattern. We first generate a mock and define how we want it to react to certain actions. Then, we plug it into our code, allowing our code to operate against it. Finally, we assert what happened to the mock, only picking the operations we care about. There is no requirement to assert every behavior experienced by the mock.
Why is this important? Record/replay requires that we record the method calls that are made by our code, third-party system, and all the other layers in the call chain. Frankly, we may not need this level of confirmation of behavior. Often, we are primarily interested in the top layer of interaction. Action/assert lets us cut back on the behavior calls we care about. We can set up our mock to generate the necessary top level actions and essentially ignore the lower level calls, which a record/replay mock would force us to record.
In this recipe, we mocked the DataAccess
operations store_cart
and retrieve_cart
. We defined their return_value
and at the end of the test, we asserted that they were called.
cart.storer.store_cart.assert_called_with(cart) cart.storer.retrieve_cart.assert_called_with(1)
cart.storer
was the internal attribute that we injected with our mock.
Because this test case focuses on storing and retrieving from the cart's perspective, we didn't have to define the real DataAccess
calls. That is why we simply put pass
in their method definitions.
This conveniently lets us work on the behavior of persistence without forcing us to choose whether the cart would be stored in a relational database, a NoSQL
database, a flat file, or any other file format. This shows that our shopping cart and data persistence are nicely decoupled.
We quickly skimmed over the useful spec
plugin for nose. It provides the same essential functionality that we coded by hand in the Naming tests so they sound like sentences and stories section. It converts test case names and test method names into readable results. It gives us a runnable spec
. This plugin works with unittest and doesn't care whether or not we were using Voidspace Mock.
Another way to phrase this question is, 'Why did we write that recipe's plugin in the first place? An important point of using test tools is to understand how they work, and how to write our own extensions. The Naming tests so they sound like sentences and stories section not only discussed the philosophy of naming tests, but also explored ways to write nose plugins to support this need. In this recipe, our focus was on using Voidspace Mock to verify certain behaviors, and not on coding nose plugins. Producing a nice BDD report was easily served by the existing spec
plugin.
18.116.14.245