rspec-given

Covering rspec-given, version 1.4.0.

rspec-given is an RSpec extension to allow Given/When/Then notation in RSpec specifications. It is a natural extension of the experimental work done on the Given framework. It turns out that 90% of the Given framework can be trivially implemented on top of RSpec.

Why Given/When/Then

RSpec has done a great job of making specifications more readable for humans. However, I really like the given / when / then nature of Cucumber stories and would like to follow the same structure in my unit tests. rspec-given allows a simple given/when/then structure RSpec specifications.

Status

rspec-given is ready for production use.

Example

Here is a specification written in the rspec-given framework:


require 'rspec/given'
require 'spec_helper'
require 'stack'

describe Stack do
  def stack_with(initial_contents)
    stack = Stack.new
    initial_contents.each do |item| stack.push(item) end
    stack
  end

  Given(:stack) { stack_with(initial_contents) }

  context "when empty" do
    Given(:initial_contents) { [] }
    Then { stack.depth.should == 0 }

    context "when pushing" do
      When { stack.push(:an_item) }

      Then { stack.depth.should == 1 }
      Then { stack.top.should == :an_item }
    end
  end

  context "with one item" do
    Given(:initial_contents) { [:an_item] }

    context "when popping" do
      When(:pop_result) { stack.pop }

      Then { pop_result.should == :an_item }
      Then { stack.should be_empty }
    end
  end

  context "with several items" do
    Given(:initial_contents) { [:second_item, :top_item] }
    Given!(:original_depth) { stack.depth }

    context "when pushing" do
      When { stack.push(:new_item) }

      Then { stack.top.should == :new_item }
      Then { stack.depth.should == original_depth + 1 }
    end

    context "when popping" do
      When(:pop_result) { stack.pop }

      Then { pop_result.should == :top_item }
      Then { stack.top.should == :second_item }
      Then { stack.depth.should == original_depth - 1 }
    end
  end
end

Let's talk about the individual statements used in the Given framework.

Given

The Given section specifies a starting point, a set of preconditions that must be true before the code under test is allowed to be run. In standard test frameworks the preconditions are established with a combination of setup methods (or :before actions in RSpec) and code in the test.

In the example code above the preconditions are started with Given statements. A top level Given (that applies to the entire describe block) says that one of the preconditions is that there is a stack with some initial contents.

Note that initial contents are not specified in the top level describe block, but are given in each of the nested contexts. By pushing the definition of "initial_contents" into the nested contexts, we can vary them as needed for that particular context.

A precondition in the form "Given(:var) ..." creates an accessor method named "var". The accessor is lazily initialized by the code block. If you want a non-lazy given, use "Given!(:var) ...".

A precondition in the form "Given ..." just executes the code block for side effects. Since there is no accessor, the code block is executed immediately (i.e. no lazy evaluation).

The preconditions are run in order of definition. Nested contexts will inherit the preconditions from the enclosing context, with out preconditions running before inner preconditions.

Given examples:


    Given(:stack) { Stack.new }

The given block is lazily run if 'stack' is ever referenced in the test and the value of the block is bound to 'stack'. The first reference to 'stack' in the specification will cause the code block to execute. Futher references to 'stack' will reuse the previously generated value.


    Given!(:original_size) { stack.size }

The code block is run unconditionally once before each test and the value of the block is bound to 'original_size'. This form is useful when you want to record the value of something that might be affected by the When code.


    Given { stack.clear }

The given block is run unconditionally once before each test. This form of given is used for code that is executed for side effects.

When

The When block specifies the code to be tested ... oops, excuse me ... specified. After the preconditions in the given section are met, the when code block is run.

There should only be one When block for a given context. However, a When in an outer context shoud be treated as a Given in an inner context. E.g.


    context "outer context" do
      When { code specified in the outer context }
      Then { assert something about the outer context }

      context "inner context" do

        # At this point, the _When_ of the outer context
        # should be treated as a _Given_ of the inner context

        When { code specified in the inner context }
        Then { assert something about the inner context }
      end
    end

When examples:


    When { stack.push(:item) }

The code block is executed once per test. The effect of the When{} block is very similar to Given{}. However, When is used to identify the particular code that is being specified in the current context or describe block.


    When(:result) { stack.pop }

The code block is executed once per test and the value of the code block is bound to 'result'. Use this form when the code under test returns a value that you wish to interrogate in the Then code.

Then

The Then sections are the postconditions of the specification. These then conditions must be true after the code under test (the When block) is run.

The code in the Then block should be a single should assertion. Code in Then blocks should not have any side effects.

Then examples:


    Then { stack.should be_empty }

After the related When block is run, the stack should be empty. If it is not empty, the test will fail.

Future Directions

I really like the way the Given framework is working out. I feel my tests are much more like specifications when I use it. However, I'm not entirely happy with it.

First, I would like to introduce invariants. An Invariant block would essentially be a post-conditions that should be true after Then block in the same (or nested) context as the invariant.

Second, I would like to remove the need for the ".should" in all the Then blocks. In other words, instead of saying:

Then { x.should == y }

we could say:

Then { x == y }

I think the wrong assertion library has laid some groundwork in this area.

Links