Writing a testable story with doctest

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.

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 see how to write a custom doctest runner to make our own report.

  1. Create a new file called recipe28_cart_with_no_items.doctest to contain our doctest scenario.
  2. Create a 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
  3. Create a new file called recipe28.py to contain our custom doctest runner.
  4. Create a customer 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.
        """
  5. Add a 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)
  6. Add a 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])
  7. Add a 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])
  8. Add a runner that replaces 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)
  9. Use the runner to exercise our scenario.
    How to do it...
  10. Use the runner to exercise our scenario with -v.
    How to do it...
  11. Alter the test scenario so that one of the expected outcomes fails.
    #there is only one item
    >>> len(cart)
    4668

    Note

    Notice we have changed the expected outcome from 1 to 4668, to guarantee a failure.

  12. Use the runner with -v again, and see the results.
    How to do it...

How it works...

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.

There's more...

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

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

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