PySpec Tutorial

Author: Shibukawa Yoshiki

Install

Requirements

PySpec needs following files.
  • Python 2.4 or 2.5
http://www.python.org/
  • wxPython 2.6(if you want to use GUI runner)
http://www.wxpython.org/download.php#binaries
  • graphviz(if you want to use GUI runner)
http://www.graphviz.org/

How to install

`python setup.py install`

PySpec Tutorial(Creating Stack)

Let's start behavior driven development. We will be creating a stack class.

Step.1 - Imagine the spec in natural language

At first, you should think the specification of stack class. You list up
following specifications.
  1. At first, the stack is empty.
  2. If you push the value to the stack, the stack is not empty.
  3. The push() method doesn't return value.
  4. The stack that have some values, you can get the last value by pop() method.
  5. If you call pop() method of the empty stack, EmptyException would be raised.

Let's implement the specifications.

Step.2 - Create Specification in Python

You should implement the specification first like TDD.

behavior_stack.py::
  import stack         # import production code
  from pyspec import * # import pyspec framework
  
  class Behavior_Stack(object):
    @context(group=1)
    def new_stack(self):
      self.stack = stack.Stack()

    @spec(group=1)
    def should_be_empty(self):
      About(self.stack).should_be_empty()

  if __name__ == "__main__":
    run_test()


The specification becomes class. In the pyspec, specification is plain python
class. In this case, the specification class has two methods. One method is
a context method, and another is specification method. You should write
preparation codes in context method. All verification methods should be in
spec method.

PySpec has many verification methods like following list:
  • should_equal()
  • should_equal_nearly()
  • should_be_true()
  • should_be_none()
  • should_be_same()
  • should_include()
  • should_be_empty()

If you want to learn more, you should see pyspec reference manual.
Then, you implement production code. you should type the interface code only
enough to run test.

stack.py::
  class Stack(object):
    def __len__(self):
      pass


If you run the spec, result would fail::
  $ python behavior_stack.py

  new stack
      should be empty ... Error

  ======================================================================
  ERROR: should be empty
  ----------------------------------------------------------------------
  Traceback (most recent call last):
    File "behavior_stack.py", line 11, in should_be_empty
      About(self.stack).should_be_empty()
    File "c:\Python25\Lib\site-packages\pyspec\__init__.py", line 502, in should_be_empty
      if len(self.actual) != 0:
  TypeError: an integer is required

  Console:

  ----------------------------------------------------------------------
  Ran 1 spec in 0.049s
  FAILED(errors=1)

Step.3 Red signal to Green

Then you should change the production code.

stack.py::
  class Stack(object):
    def __len__(self):
      return 0


In this time, the result would be success::
  $ python behavior_stack.py

  new stack
      should be empty ... OK

  ----------------------------------------------------------------------
  Ran 1 spec in 0.048s
  OK


The implementation is temporary code, but implementing this spec was finished.
After implementing all specs, the production code will be completed.

Step.4 Store one value(1)

Next, you implement second spec.

behavior_stack.py::
  class Behavior_Stack(object):
    @context(group=2)
    def stack_with_one_value(self):
      self.stack = stack.Stack()
      self.stack.push(10)

    @spec(group=2)
    def should_not_be_empty(self):
      About(self.stack).should_not_be_empty()

And you should implement the interface.

stack.py::
  class Stack(object):
    def push(self, value):
      pass


Then, If you run the spec, the spec will fail as you imagine::
  $ python behavior_stack.py

  new stack
      should be empty ... OK
  stack with one value
      should not be empty ... Failure

  ======================================================================
  FAIL: should not be empty
  ----------------------------------------------------------------------
  Traceback (most recent call last):
    File "behavior_stack.py", line 20, in should_not_be_empty
      About(self.stack).should_not_be_empty()
  AssertionError: self.stack should not be empty, but was empty.

  Console:

  ----------------------------------------------------------------------
  Ran 2 specs in 0.062s
  FAILED(failures=1)

Step.5 Store one value(2)

You check the spec test fail. Let's make the red signal to green.

stack.py::
  class Stack(object):
    def __init__(self):
      self.values = []

    def __len__(self):
      return len(self.values)

    def push(self, value):
      self.values.append(value)


The result become::
  $ python behavior_stack.py

  new stack
      should be empty ... OK
  stack with one value
      should not be empty ... OK

  ----------------------------------------------------------------------
  Ran 2 specs in 0.062s
  OK


Fail the spec test at first is important. If you mistake to implement the
test, the spec doesn't fail. It declare the spec is right or not.

Step.6 push() doesn't return value

It's easy to create the spec.

behavior_stack.py::
  class Behavior_Stack(object):
    @spec(group=1)
    def push_should_not_return_value(self):
        About(self.stack.push(1)).should_be_none()


And then, try to run the spec::
  $ python behavior_stack.py
  
  new stack
      should be empty ... OK
  stack with one value
      should not be empty ... OK
  new stack
      push should not return value ... OK


New spec was passed to the test even if you have done nothing.
But writing spec is important. If the product code would be changed,
the spec can find errors.

Step.7 You can get the last value by pop() method

behavior_stack.py::
  class Behavior_Stack(object):
    @context(group=3)
    def stack_with_some_values(self):
      self.stack = stack.Stack()
      self.stack.push(10)
      self.last_value = 20
      self.stack.push(self.last_value)

    @spec(group=3)
    def should_return_last_value_by_pop_method(self):
      About(self.stack.pop()).should_equal(self.last_value)


Of course, you can write following code::

    @spec(group=3)
    def should_return_last_value_by_pop_method(self):
      About(self.stack.pop()).should_equal(20)


These specs have same affect to the interpreter. But first spec has
more information to understand the intention of the spec(last_value is 20).

After running the spec and watch the red signal, you will write the following
code.

stack.py::
  class Stack(object):
    def pop(self):
      value = self.values[-1]
      del self.values[-1]
      return value

result::
  $ python behavior_stack.py
  
  new stack
      should be empty ... OK
  stack with one value
      should not be empty ... OK
  new stack
      push should not return value ... OK
  stack with some values
      should return last value by pop method ... OK
  
  ----------------------------------------------------------------------
  Ran 4 specs in 0.031s
  OK

Step.8 EmptyException would be raised

This is the last step. You will create the spec for exception.
At first, You should create the exception class.

stack.py::
  class EmptyException(RuntimeError):
      pass

And then, you create spec.

behavior_stack.py::
  class Behavior_Stack(object):
    @context(group=4)
    def empty_stack(self):
        self.empty_stack = stack.Stack()

    @spec(group=4, expect=stack.EmptyException)
    def should_raise_EmptyException_if_pop_called(self):
      self.empty_stack.pop()


@spec can accept ``expect`` parameter. If you set this parameter,
that spec method describes the specification of exceptions. In this
case, exception whose class is stack.EmptyException will be raised
if the product code would have been implemented collectly.

You implement the product code after watching that the spec failed.

stack.py::
  class Stack(object):
      def pop(self):
          if self.values:
              value = self.values[-1]
              del self.values[-1]
              return value
          raise EmptyException()


In this step, there is another topic. You already have created
the context that have new stack(group=1). But, you wrote another
context method. It makes the spec description clearer. "empty stack"
is more suitable than "new stack".

Step.9 Refactoring

You have finished all specifications. Finally, you refactor the
specs. It is important for the specification as a document.

Spec name is important for concept of BDD.
At first, check the output messages. Is there bad spec name as
natural language? If you find bad spec names, you should fix them.

If the spec class is too large, you need to split. ``group`` parameter
helps you. All @context and @spec decorator in this tutorial has
group parameter. But if all decorator's group parameter are same,
you can skip them.

behavior_stack.py::
  class Behavior_Stack_New(object):
      @context
      def new_stack(self):
          self.stack = stack.Stack()

      @spec
      def should_be_empty(self):
          About(self.stack).should_be_empty()

      @spec
      def push_method_should_not_return_value(self):
          About(self.stack.push(1)).should_be_none()


  class Behavior_Stack_WithValue(object):
      @context
      def stack_with_one_value(self):
          self.stack = stack.Stack()
          self.stack.push(10)

      @spec
      def should_not_be_empty(self):
          About(self.stack).should_not_be_empty()


That is all this document tells you. Thank you for reading my poor English.
Have fun!

Last edited Jan 26, 2008 at 2:04 AM by shibu, version 2

Comments

No comments yet.