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.
For this recipe, we will be using the shopping cart application shown at the beginning of this chapter.
With the following steps, we will explore how to write a custom nose plugin that formats results as a BDD-style report.
recipe26.py
to contain our test cases.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)
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)
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)
recipe26_plugin.py
to contain our customized BDD runner.–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
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)
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! ")
recipe26_runner.py
to contain a test runner for exercising this recipe.if __name__ == "__main__": import nose from recipe26_plugin import * nose.run(argv=["", "recipe26", "--with-bdd"], plugins=[BddPrinter()])
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)
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.
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.
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
3.15.189.199