Naming tests that sound like sentences and stories

Test methods should read like sentences and test cases should read like titles of chapters. This is part of BDD's philosophy of making tests easy-to-read for non-programmers.

Getting ready

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

How to do it...

With the following steps, we will explore how to write a custom nose plugin that formats results as a BDD-style report.

  1. Create a file called recipe26.py to contain our test cases.
  2. Create a unittest test where the test case represents a cart with one item, and the test methods read like sentences.
    import unittest
    from cart import *
    
    class CartWithOneItem(unittest.TestCase):
        def setUp(self):
            self.cart = ShoppingCart().add("tuna sandwich", 15.00)
    
        def test_when_checking_the_size_should_be_one_based(self):
            self.assertEquals(1, len(self.cart))
    
        def test_when_looking_into_cart_should_be_one_based(self):
            self.assertEquals("tuna sandwich", self.cart.item(1))
            self.assertEquals(15.00, self.cart.price(1))
    
        def test_total_should_have_in_sales_tax(self):
            self.assertAlmostEquals(15.0*1.0925, 
                                    self.cart.total(9.25), 2)
  3. Add a unittest test where the test case represents a cart with two items, and the test methods read like sentences.
    class CartWithTwoItems(unittest.TestCase):
        def setUp(self):
            self.cart = ShoppingCart() 
                             .add("tuna sandwich", 15.00) 
                             .add("rootbeer", 3.75)
        def test_when_checking_size_should_be_two(self):
            self.assertEquals(2, len(self.cart))
    
        def test_items_should_be_in_same_order_as_entered(self):
            self.assertEquals("tuna sandwich", self.cart.item(1))
            self.assertAlmostEquals(15.00, self.cart.price(1), 2)
            self.assertEquals("rootbeer", self.cart.item(2))
            self.assertAlmostEquals(3.75, self.cart.price(2), 2)
    
        def test_total_price_should_have_in_sales_tax(self):
            self.assertAlmostEquals((15.0+3.75)*1.0925, 
                                    self.cart.total(9.25), 2)
  4. Add a unittest test where the test case represents a cart with no items, and the test methods read like sentences.
    class CartWithNoItems(unittest.TestCase):
        def setUp(self):
            self.cart = ShoppingCart()
    
        def test_when_checking_size_should_be_empty(self):
            self.assertEquals(0, len(self.cart))
    
        def test_finding_item_out_of_range_should_raise_error(self):
            self.assertRaises(IndexError, self.cart.item, 2)
    
        def test_finding_price_out_of_range_should_raise_error(self):
            self.assertRaises(IndexError, self.cart.price, 2)
    
        def test_when_looking_at_total_price_should_be_zero(self):
            self.assertAlmostEquals(0.0, self.cart.total(9.25), 2)
    
        def test_adding_items_returns_back_same_cart(self):
            empty_cart = self.cart
            cart_with_one_item = self.cart.add("tuna sandwich", 
                                                            15.00)
            self.assertEquals(empty_cart, cart_with_one_item)
            cart_with_two_items = self.cart.add("rootbeer", 3.75)
            self.assertEquals(empty_cart, cart_with_one_item)
            self.assertEquals(cart_with_one_item, 
                              cart_with_two_items)

    Note

    BDD encourages using very descriptive sentences for method names. Several of these method names were shortened to fit the format of this book.

  5. Create another file called recipe26_plugin.py to contain our customized BDD runner.
  6. Create a nose plugin that can be used as –with-bdd to print out results.
    import sys
    err = sys.stderr
    
    import nose
    import re
    from nose.plugins import Plugin
    
    class BddPrinter(Plugin):
        name = "bdd"
    
        def __init__(self):
            Plugin.__init__(self)
            self.current_module = None
  7. Create a handler that prints out either the module or the test method, with extraneous information stripped out.
        def beforeTest(self, test):
            test_name = test.address()[-1]
            module, test_method = test_name.split(".")
            if self.current_module != module:
                self.current_module = module
                fmt_mod = re.sub(r"([A-Z])([a-z]+)", 
                                 r"12 ", module)
                err.write("
    Given %s" % fmt_mod[:-1].lower())
            message = test_method[len("test"):]
            message = " ".join(message.split("_"))
            err.write("
    - %s" % message)
  8. Create a handler for success, failure, and error messages.
        def addSuccess(self, *args, **kwargs):
            test = args[0]
            err.write(" : Ok")
    
        def addError(self, *args, **kwargs):
            test, error = args[0], args[1]
            err.write(" : ERROR!
    ")
    
        def addFailure(self, *args, **kwargs):
            test, error = args[0], args[1]
            err.write(" : Failure!
    ")
  9. Create a new file called recipe26_runner.py to contain a test runner for exercising this recipe.
  10. Create a test runner that pulls in the test cases and runs them through nose, printing out results in an easy-to-read fashion.
    if __name__ == "__main__":
        import nose
        from recipe26_plugin import *
    
        nose.run(argv=["", "recipe26", "--with-bdd"], 
                                plugins=[BddPrinter()])
  11. Run the test runner.
    How to do it...
  12. Introduce a couple of bugs in the test cases, and re-run the test runner to see how this alters the output.
        def test_when_checking_the_size_should_be_one_based(self):
            self.assertEquals(2, len(self.cart))
    ...
        def test_items_should_be_in_same_order_as_entered(self):
            self.assertEquals("tuna sandwich", self.cart.item(1))
            self.assertAlmostEquals(14.00, self.cart.price(1), 2)
            self.assertEquals("rootbeer", self.cart.item(2))
            self.assertAlmostEquals(3.75, self.cart.price(2), 2)
  13. Run the tests again.
    How to do it...

How it works...

The test cases are written as nouns, describing the object being tested. CartWithTwoItems describes a series of test methods centered on a shopping cart that is pre-populated with two items.

The test methods are written like sentences strung together with underscores instead of spaces. They have to be prefixed with test_, so that unittest will pick them up. test_items_should_be_in_same_order_as_entered should represent "items should be in same order as entered"

The idea is we should be able to quickly understand what is being tested by putting these two together: Given a cart with two items, the items should be in the same order as entered.

While we could read through the test code with this thought process, mentally subtracting out the cruft of underscores and the test prefix, this can become a real cognitive load for us. To make it easier, we coded a quick nose plugin that split up the camel case tests and replaced the underscores with spaces. This led to the useful report format.

Using this type of quick tool encourages us to write detailed test methods that will be easy to read on output. The feedback not just to us but to our test team and customers can be very effective at fostering communications, confidence in software, and help with generating new test stories.

There's more

The example test methods shown here were deliberately shortened to fit the format of the book. Don't try to make them as short as possible. Instead, try to descriptively describe the expected output.

The plugin isn't installable

This plugin was coded to quickly generate a report. To make it reusable especially with nosetests you may want to read Running automated test suites with nose mentioned in Chapter 2 to get more details on creating a setup.py script to support the installation.

See also

Writing a nose extension to pick tests based on regular expressions; and Writing a nose extension to generate a CSV report as discussed in Chapter 2

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

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