Capturing a succinct story in a doctest
file is the key to BDD. Another aspect of BDD is providing a readable report including the results.
For this recipe, we will be using the shopping cart application shown at the beginning of this chapter.
With the following steps, we will see how to write a custom doctest
runner to make our own report.
recipe28_cart_with_no_items.doctest
to contain our doctest
scenario.doctest
scenario that exercises the shopping cart.This scenario demonstrates a testable story. First, we need to import the modules >>> from cart import * >>> cart = ShoppingCart() #when we add an item >>> cart.add("carton of milk", 2.50) #doctest:+ELLIPSIS <cart.ShoppingCart object at ...> #the first item is a carton of milk >>> cart.item(1) 'carton of milk' #the first price is $2.50 >>> cart.price(1) 2.5 #there is only one item >>> len(cart) 1 This shopping cart lets us grab more than one of a particular item. #when we add a second carton of milk >>> cart.add("carton of milk", 2.50) #doctest:+ELLIPSIS <cart.ShoppingCart object at ...> #the first item is still a carton of milk >>> cart.item(1) 'carton of milk' #but the price is now $5.00 >>> cart.price(1) 5.0 #and the cart now has 2 items >>> len(cart) 2 #for a total (with 10% taxes) of $5.50 >>> cart.total(10.0) 5.5
recipe28.py
to contain our custom doctest
runner.doctest
runner by subclassing DocTestRunner
.import doctest. class BddDocTestRunner(doctest.DocTestRunner): """ This is a customized test runner. It is meant to run code examples like DocTestRunner, but if a line preceeds the code example starting with '#', then it prints that comment. If the line starts with '#when', it is printed out like a sentence, but with no outcome. If the line starts with '#', but not '#when' it is printed out indented, and with the outcome. """
report_start
function that looks for comments starting with #
before an example.def report_start(self, out, test, example): prior_line = example.lineno-1 line_before = test.docstring.splitlines()[prior_line] if line_before.startswith("#"): message = line_before[1:] if line_before.startswith("#when"): out("* %s " % message) example.silent = True example.indent = False else: out(" - %s: " % message) example.silent = False example.indent = True else: example.silent = True example.indent = False doctest.DocTestRunner(out, test, example)
report_success
function that conditionally prints out ok
.def report_success(self, out, test, example, got): if not example.silent: out("ok ") if self._verbose: if example.indent: out(" ") out(">>> %s " % example.source[:-1])
report_failure
function that conditionally prints out FAIL
.def report_failure(self, out, test, example, got): if not example.silent: out("FAIL ") if self._verbose: if example.indent: out(" ") out(">>> %s " % example.source[:-1])
doctest.DocTestRunner
with our customer runner, and then looks for doctest files to run.if __name__ == "__main__": from glob import glob doctest.DocTestRunner = BddDocTestRunner for file in glob("recipe28*.doctest"): given = file[len("recipe28_"):] given = given[:-len(".doctest")] given = " ".join(given.split("_")) print "===================================" print "Given a %s..." % given print "===================================" doctest.testfile(file)
-v
.#there is only one item >>> len(cart) 4668
-v
again, and see the results.
Doctest
provides a convenient means to write a testable scenario. For starters, we wrote up a series of behaviors we wanted the shopping cart application to prove. To polish things up, we added lot of detailed comments, so that anyone reading this document can clearly understand things.
This provides us with a testable scenario. However, it leaves us short of one key thing: a succinct report.
Unfortunately, doctest
won't print out all these detailed comments for us.
To make this usable from a BDD perspective, we need the ability to embed selective comments that get printed out when the test sequence runs. To do that we will subclass doctest.DocTestRunner
and insert our version of handling of the docstring
.
DocTestRunner
conveniently gives us a handle on the docstring
as well as the exact line number where the code example starts. We coded our BddDocTestRunner
to look at the line preceding it, and check to see if it started with #
, our custom marker for a piece of text to print out during a test run.
A #when
comment is considered a cause. In other words, a when causes one or more effects. While doctest
will still verify the code involved with a when; for BDD purposes, we don't really care about the outcome, so we silently ignore it.
Any other #
comments are considered effects. For each of these, we strip out the #
then print the sentence indented, so we can easily see which when it is tied to. Finally, we print out either ok
or FAIL
to indicate the results.
This means we can add all the detail we want to the documentation. But for blocks of tests, we can add statements that will be printed as either causes (#when
) or effects (#anything else
).