Lessons in Spec'ing 6

Posted by David Fri, 26 Oct 2007 13:30:14 GMT

Lesson: system calls in specs do stuff on your system. Be careful.

I was trying different things to understand why a spec was failing and, at one point, put a pending statement in a spec that generates a file and then deletes it. The statement that deletes the file is in an after(:each) block (which is guaranteed to run) and looks like this:

system(%Q|rm "#{dir}/#{filename}"|)

Imagine my horror when I saw this in the output:

rm: /: is a directory

Plain Text Stories: Part III 5

Posted by David Thu, 25 Oct 2007 08:52:20 GMT

Here’s the latest update to Plain Text Stories. Effective r2789 in RSpec’s trunk.

Step 1: Write a Story

Story: simple addition

  As an accountant
  I want to add numbers
  So that I can count beans

  Scenario: add one plus one
    Given an addend of 1
    And an addend of 1

    When the addends are added

    Then the sum should be 2
    And the corks should be popped

  Scenario: add two plus five
    Given an addend of 2
    And an addend of 5

    When the addends are added

    Then the sum should be 7

Step 2: Create Steps

# This creates steps for :addition
steps_for(:addition) do
  Given("an addend of $addend") do |addend|
    @adder ||= Adder.new
    @adder << addend.to_i
  end
end

# This appends to them
steps_for(:addition) do
  When("the addends are added")  { @sum = @adder.sum }
  Then("the sum should be $sum") { |sum| @sum.should == sum.to_i }
end

Step 3: Let her open the box … no, that’s not it …

Step 3: Run the Story with the steps you want (adding any that are only for this story as you go).

with_steps_for :addition do
  Then("the corks should be popped") {}
  run 'path/to/story/file'
end

Working with Rails?

with_steps_for :navigation do
  run 'path/to/story/file', :type => RailsStory
end

What about multiple groups of steps?


with_steps_for :login, :navigation, :form_submissions do
  run 'path/to/story/file'
end

Coming soon to a computer near you … (as soon as you can “seven up”)

Plain Text Stories on Rails 7

Posted by David Tue, 23 Oct 2007 03:44:00 GMT

Since my last post on plain text stories, there have already been a few improvements, not the least of which is that it will now work with Rails. Again, this is trunk (rev 2769+) only and experimental.

Here’s a working example from an app that I’m working on:

stories/login
Story: registered user logs in
  As a registered user
  I want to have to log in
  So that only other registered users can see my data

  Scenario: user logs in and sees welcome page
    Given a user registered with login: foo and password: test
    When user logs in with login: foo and password: test
    Then user should see the welcome page

  Scenario: user logs in with wrong password
    Given a user registered with login: foo and password: test
    When user logs in with login: foo and password: wrong
    Then user should see the login form
    And page should include text: There was an error logging in.

  Scenario: user logs in with wrong login name
    Given a user registered with login: foo and password: test
    When user logs in with login: wrong and password: test
    Then user should see the login form
    And page should include text: There was an error logging in.

[Update: modified to use runner.steps instead of runner.step_matchers]

stories/login.rb
require File.join(File.dirname(__FILE__), *%w[helper])

run_story :type => RailsStory do |runner|
  runner.steps << LoginSteps.new
  runner.steps << NavigationSteps.new
  runner.load File.expand_path(__FILE__).gsub(".rb","")
end

Here’s what’s new in this example:

  • run_story is added to the main object so you don’t have to remember that silly path to the PlainTextStoryRunner which will undoutedbly change!
  • run_story accepts arguments, including an options hash, which it will pass to the constructor of the PlainTextStoryRunner (in this case, :type => RailsStory)
  • run_story yields the runner, which now supports a load method which you use to tell it where to find the plain text story file.
  • run_story … runs the story

Keep your eyes peeled for more updates in the coming days.

Story Runner in Plain English 21

Posted by David Mon, 22 Oct 2007 00:29:00 GMT

Houston, we have Plain Text!

I just committed a first stab at a Plain Text Story Runner. It’s in RSpec’s trunk and will be (in some form) part of the next release.

Big thanks to Pat Maddox for the StoryPartFactory (which is now called StoryMediator) and to all on the rspec-users list who contributed their ideas and thoughts to the discussion about plain text stories.

Keep in mind that this is brand new and very experimental. I do not recommend that you start converting all your projects to using this.

That said …

A bit of background

[Update: modified to use And for multiple Givens, Whens or Thens]

The initial implementation of Story Runner supported syntax like this (slightly modified from Dan North’s article introducing rbehave):

Story "transfer to cash account",
%(As a savings account holder
  I want to transfer money from my savings account
  So that I can get cash easily from an ATM) do

  Scenario "savings account is in credit" do
    Given "my savings account balance is", 100 do |balance|
      @savings_account = Account.new(balance)
    end
    And "my cash account balance is", 10 do |balance|
      @cash_account = Account.new(balance)
    end
    When "I transfer", 20 do |amount|
      @savings_account.transfer_to(@cash_account, amount)
    end
    Then "my savings account balance should be", 80 do |expected_amount|
      @savings_account.balance.should == expected_amount
    end
    And "my cash account balance should be", 30 do |expected_amount|
      @cash_account.balance.should == expected_amount
    end
  end

  Scenario "savings account is overdrawn" do
    Given "my savings account balance is", -20
    And "my cash account balance is", 10
    When "I transfer", 20
    Then "my savings account balance should be", -20
    And "my cash account balance should be", 10
  end
end

While this is a really cool start, there are a couple of problems. One is that we’re constrained in the way we phrase things. Because the arguments become part of the phrase, we have to structure each phrase so that the argument comes at the end.

The other problem, for me, is that the differing levels of abstraction in the two scenarios make it difficult to read.

Enter Blockless Steps and Parameterized Steps

The first step in resolving this problem was to decouple the expression of the story from the steps, which is accomplished with the use of Parameterized Steps. Here’s how the story above might look:

[Update: using StepGroup/define instead of StepMatchers/add]

steps = StepGroup.new do |define|
  define.given("my savings account balance is $balance") do |balance|
    @savings_account = Account.new(balance.to_f)
  end

  define.given("my cash account balance is $balance" do |balance|
    @cash_account = Account.new(balance.to_f)
  end

  define.when("I transfer $amount") do |amount|
    @savings_account.transfer_to(@cash_account, amount.to_f)
  end

  define.then("my savings account balance should be $expected_amount" do |expected_amount|
    @savings_account.balance.should == expected_amount.to_f
  end

  define.then("my cash account balance should be $expected_amount" do |expected_amount|
    @cash_account.balance.should == expected_amount.to_f
  end

end

Story "transfer to cash account",
  %(As a savings account holder
    I want to transfer money from my savings account
    So that I can get cash easily from an ATM),
    :steps => steps do

  Scenario "savings account is in credit" do
    Given "my savings account balance is 100" 
    And "my cash account balance is 10" 
    When "I transfer 20" 
    Then "my savings account balance should be 80" 
    And "my cash account balance should be 30" 
  end

  Scenario "savings account is overdrawn" do
    Given "my savings account balance is -20" 
    And "my cash account balance is 10" 
    When "I transfer 20" 
    Then "my savings account balance should be -20" 
    And "my cash account balance should be 10" 
  end
end

A bit nicer, yes? The steps coming first is a bit noisy, but that could be extracted to another file, or perhaps we can add a means of associating them with the Story after the Story has already been parsed so they can move below the Story.

That bit aside, look how much cleaner the Story reads now. And we can do a couple of additional things to make it even nicer. One thing you might notice is that the line about transfering (When “I transfer 20”) doesn’t specify which way the transfer goes. We can improve on that by enhancing that step:

steps = StepGroup.new do |define|
  ...

  define.when("I transfer $amount from $source to $target") do |amount, source, target|
    if source == 'cash' and target == 'savings'
      @savings_account.transfer_to(@cash_account, amount.to_f)
    elsif source == 'savings' and target == 'cash'
      @cash_account.transfer_to(@savings_account, amount.to_f)
    else
      raise "I don't know how to transfer from #{source} to #{target}" 
    end
  end

  ...

That lets us write the step as

When "I transfer 20 from savings to cash"

As you can see, this is a big step towards making stories more clear and flexible.

More on Steps

Another thing you may have noticed is that the Steps are grouped together somewhat arbitrarily. Thanks to a couple of handy convenience methods, you can easily build up libraries of these steps and make them as broad or as granular as you like. Perhaps we want the account steps available to many stories, but the transfer step only to this one. Here’s how you can handle that:

class AccountSteps < Spec::Story::StepGroup
  steps do |define|
    define.given("my savings account balance is $balance") do |balance|
      @savings_account = Account.new(balance.to_f)
    end

    define.given("my cash account balance is $balance" do |balance|
      @cash_account = Account.new(balance.to_f)
    end

    define.then("my savings account balance should be $expected_amount" do |expected_amount|
      @savings_account.balance.should == expected_amount.to_f
    end

    define.then("my cash account balance should be $expected_amount" do |expected_amount|
      @cash_account.balance.should == expected_amount.to_f
    end
  end
end

steps = AccountSteps.new do |define|
  define.when("I transfer $amount") do |amount|
    @savings_account.transfer_to(@cash_account, amount.to_f)
  end
end

Here we’ve created a subclass of StepGroup, instantiated one and defined an additional ‘when’ that will only be available to this instance.

Goodbye quotes!

Once we were able to get rid of the blocks, the quotes made no sense. So we’ve added support for true plain text stories. So now our example can read like this:

Story: transfer to cash account
  As a savings account holder
  I want to transfer money from my savings account
  So that I can get cash easily from an ATM

  Scenario: savings account is in credit
    Given my savings account balance is 100
    And my cash account balance is 10
    When I transfer 20
    Then my savings account balance should be 80
    And my cash account balance should be 30

  Scenario: savings account is overdrawn
    Given my savings account balance is -20
    And my cash account balance is 10
    When I transfer 20
    Then my savings account balance should be -20
    And my cash account balance should be 10

That gets stored in a plain text file and you can run it by running a ruby file that looks like this:

require 'spec'
require 'path/to/your/library/files'
require 'path/to/file/that/defines/account_steps.rb'

# assumes the other story file is named the same as this file minus ".rb" 
runner = Spec::Story::Runner::PlainTextStoryRunner.new(File.expand_path(__FILE__).gsub(".rb",""))
runner.steps << AccountSteps.new
runner.run

And that’s it! It’s that simple. This is still in its very early phases and I’m certain there will be enhancements as people gain experience with it.

If you want to check it out yourself, grab the trunk and do the following:

cd trunk/rspec
ruby examples/stories/calculator.rb
ruby examples/stories/addition.rb

The first example uses Ruby with blockless steps. The second example uses a plain text story stored in examples/stories/addition.

Also, with a couple of small tweaks we’ll be able to consume plain text from any source (not just a local file) and feed it into the PlainTextStoryRunner. This means that we’ll be able to do things like email scenarios to an app that consumes email and runs the scenario against the system and emails you back a report! Crazy, huh?

Lastly, just a reminder, this is only in trunk right now (as of rev 2764), so if you want to explore it you’ll have to get the trunk.

Enjoy!!!!!