Writing a testable story with Voidspace Mock and nose

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).

Note

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.

Getting ready

For this recipe, we will be using the shopping cart application shown at the beginning of this chapter with some slight modifications.

  1. Create a new file called recipe30_cart.py and copy all the code from cart.py created in the introduction of this chapter.
  2. Alter __init__ to add an extra storer attribute used for persistence.
    class ShoppingCart(object):
        def __init__(self, storer=None):
            self.items = []
            self.storer = storer
  3. Add a store method that uses the storer to save the cart.
        def store(self):
            return self.storer.store_cart(self)
  4. Add a 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

Note

The specifics of the API of the storer will be given further down in this recipe.

We need to activate our virtual environment and then install Voidspace Mock for this recipe.

  1. Create a virtual environment, activate it, and verify the tools are working.
    Getting ready
  2. Install voidspace mock by typing pip install mock.
  3. Install the latest version of pinocchio by typing pip install http://darcs.idyll.org/~t/projects/pinocchio-latest.tar.gz.
  4. This version of pinocchio raises some warnings. To prevent them, we also need to install figleaf by typing pip install figleaf.

How to do it...

With the following steps, we will explore how to use mock to write a testable story:

  1. In 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
  2. Create a new file called recipe30.py to write test code.
  3. Create an automated unittest that exercises the cart by mocking out the methods of 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)
  4. Run the test using nosetests with the spec plugin.
    How to do it...

How it works...

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.

Tip

Mocking a method: This means replacing a call to a real method with one to a mock object.

Stubbing a method: This means replacing a call to a real method with one to a stub.

There's more...

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.

Tell me more about the spec nose plugin!

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.

Why didn't we reuse the plugin from the recipe 'Naming tests so they sound like sentences and stories'?

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.

See also

Writing a testable story with mockito and nose

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.14.245