11 minute read

I’ve now progressed through the first six chapters of the Ruby on Rails Tutorial and my head is spinning. I feel like Keanu Reeve’s character from the 90’s b-movie Johnny Mnemonic. Before I proceed, I thought I’d take some time to digest what I’ve learned and do a little research into aspects that I’ve found somewhat inscrutable, which are legion. Where to start? I’m somewhat intrigued by the idea of Test Driven Development, or in the case of the Ruby on Rails tutorial its variant which is described as Behavior Driven Development.

Behavior Driven Development

Behavior Driven Development or BDD is a framework for unit testing of software that seeks to rephrase test cases using more natural language. Unit tests are described as examples of how the system should behave. BDD is not a radical re-invention of Test Driven Development, but rather an extension or re-interpretation of it. To understand more about how Behavior Driven Development came about, I recommend reading the article Introducing BDD by Dan North.

RSpec

RSpec is a Behavior Driven Development tool for Ruby inspired by the work of Dan North and JBehave. This tool enables the execution of human readable test specifications and provides methods to generate readable test result documentation. To understand more about RSpec, I recommend the following articles:

A Simple Example

The Ruby on Rails Tutorial contains an excellent overview of BDD and RSpec and interweaves testing using RSpec throughout each chapter. Rails is set up out of the box to incorporate a unit testing framework like RSpec.

It is possible to use RSpec without the Rails framework, however. This simplifies things considerably but still allows for a useful demonstration of what RSpec is all about.

Please note that the examples that follow are geared for the Mac OS X environment. I’m assuming you’ve already installed Ruby and Gem. You’ll also need to have RSpec installed, so open a command shell and type the following command:

$ gem install rspec

You should get a response like the following:

Successfully installed rspec-2.8.0
1 gem installed
Installing ri documentation for rspec-2.8.0...
Installing RDoc documentation for rspec-2.8.0...

You may want to create a directory where for our test files at this point:

mkdir rspec_example
cd rspec_example

When doing test driven development the typical workflow is a follows:

  • Write a test that describes the behavior of a part of the system.
  • Run the test ensuring that it fails because the code has not yet been written.
  • Write just enough code to make the test pass.
  • Run the test to confirm that the test now passes.

If necessary, the code can then be refactored without changing its function. The validity of the refactored code can then be confirmed by running the test again.

I’ll use a BankAccount class to demonstrate the concept behind test driven development. The BankAccount class keeps track of someone’s bank balance and provides methods to handle deposits and withdrawals.

Let’s start by creating the test specification for our (yet to be developed) BankAccount class. Open your favorite text editor and save the following as bankaccount_spec.rb:

require './bankaccount'

describe BankAccount do
end

Note the “./” preceding the file name for the BankAccount class. This is necessary when referring to a file within the same directory.

The describe method creates an ExampleGroup for the BankAccount class. At this point we haven’t created any examples yet, but our class behavior examples will later be defined within this block.

Let’s run RSpec to confirm that the test fails as expected. At the shell prompt enter the command to run RSpec for our bankaccount_spec.rb:

$ rspec bankaccount_spec.rb
cannot load such file -- ./bankaccount (LoadError)

The test failed as expected since the file doesn’t exist yet. Let’s remedy this by creating the BankAccount.rb file:

class BankAccount
end

Not very interesting yet, but our test should pass this time. From the shell prompt run the rspec command again:

$ rspec bankaccount_spec.rb
No examples found.

Finished in 0.00004 seconds
0 examples, 0 failures

Our spec contained no examples, so the result “0 examples, 0 failures” is the expected result. Whew…

When a new BankAccount instance is created, it should start with a zero balance. Let’s create our first example to validate this behavior:

Edit the bankaccount_spec.rb file to add the following statements:

require './bankaccount'

describe BankAccount do

  it "sets initial balance to zero" do
    @account = BankAccount.new
    @account.balance.should eq(0.00)
  end

end

So, what’s up with “it?” it turns out to be a method that returns an instance of an Example. it takes two parameters, a name, and a block that contains our test. In this test, we’re creating an instance of BankAccount and then verifying that the balance is equal to zero.

Now, when we run RSpec again, we should get an error, since the balance method has not yet been defined in our BankAccount class.

NOTE: You can use the --format documentation option with rspec to display example names in the output.

$ rspec bankaccount_spec.rb --format documentation

BankAccount
  sets initial balance to zero (FAILED - 1)

Failures:

  1) BankAccount sets initial balance to zero
     Failure/Error: @account.balance.should eq(0.00)
     NoMethodError:
       undefined method `balance' for #<BankAccount:0x007f92db1f4850>
     # ./bankaccount_spec.rb:7:in `block (2 levels) in <top (required)>'

Finished in 0.00055 seconds
1 example, 1 failure

Failed examples:

rspec ./bankaccount_spec.rb:5 # BankAccount sets initial balance to zero

Now we’ll add the code to our BankAccount.rb file to get this test to pass:

class BankAccount
  def initialize
    @balance = 0.00
  end

  def balance
    @balance
  end
end

The initialize constructor for our BankAccount class sets the balance to zero. We’ve also defined the balance getter method for our balance property. When we run the test again it should pass:

$ rspec bankaccount_spec.rb --format documentation

BankAccount
  sets initial balance to zero

Finished in 0.0006 seconds
1 example, 0 failures

Voila! Let’s next add an example to our spec for the deposit method. Each example describes an expected behavior of the system under test. The expected behavior of the deposit method is that the balance will have increased by the amount of the deposit.

Edit the bankaccount_spec.rb file to add the deposit example:

require './bankaccount'

describe BankAccount do

  it "sets initial balance to zero" do
    @account = BankAccount.new
    @account.balance.should eq(0.00)
  end

  it "increases balance by amount of deposit" do
    @account = BankAccount.new
    @account.deposit 100.00
    @account.balance.should eq(100.00)
  end

end

In this test, we’re creating an instance of BankAccount and calling its deposit method with a value of 100.00. The expected behavior of this method call is that the balance for the account has increased by 100.00. We’re validating the result using the should method which takes eq(100) as a parameter.

Now, when we run RSpec again, we should get an error, since the deposit method has not yet been defined in our BankAccount class.

$ rspec bankaccount_spec.rb --format documentation

BankAccount
  sets initial balance to zero
  increases balance by amount of deposit (FAILED - 1)

Failures:

  1) BankAccount increases balance by amount of deposit
     Failure/Error: @account.deposit 100.00
     NoMethodError:
       undefined method `deposit' for #<BankAccount:0x007fa3aa350688 @balance=0.0>
     # ./bankaccount_spec.rb:12:in `block (2 levels) in <top (required)>'

Finished in 0.00187 seconds
2 examples, 1 failure

Failed examples:

rspec ./bankaccount_spec.rb:10 # BankAccount increases balance by amount of deposit

Now let’s try to get this test to pass by defining the deposit method. Edit the BankAccount.rb file to include the deposit method:

class BankAccount

  def initialize
    @balance = 0.00
  end

  def balance
    @balance
  end

  def deposit (amount)
    @balance += amount
  end

end

Now when we run RSpec again, the test should pass:

$ rspec bankaccount_spec.rb --format documentation

BankAccount
  sets initial balance to zero
  increases balance by amount of deposit

Finished in 0.00063 seconds
2 examples, 0 failures

Wow…that actually worked!

At this point let’s take a moment to refactor some of the code in our spec. RSpec allows you to define a before block where you can perform setup tasks. You may have noticed that in each of the examples in our test we are creating an instance of BankAccount. Let’s put that in a before block at the top of our bankaccount_spec:

require './bankaccount'

describe BankAccount do

  before :each do
    @account = BankAccount.new
  end

  it "sets initial balance to zero" do
    @account.balance.should eq(0.00)
  end

  it "increases balance by amount of deposit" do
    @account.deposit 100.00
    @account.balance.should eq(100.00)
  end

end

The :each symbol indicates that this setup block should be executed before each test.

Let’s run rspec again to be sure that this didn’t break anything:

$ rspec bankaccount_spec.rb --format documentation

BankAccount
  sets initial balance to zero
  increases balance by amount of deposit

Finished in 0.00065 seconds
2 examples, 0 failures

Looking good! Buoyed by this success, let’s add an example for the as yet to be defined withdraw method to the bankaccount_spec.rb file:

require './bankaccount'

describe BankAccount do

  before :each do
    @account = BankAccount.new
  end

  it "sets initial balance to zero" do
    @account.balance.should eq(0.00)
  end

  it "increases balance by amount of deposit" do
    @account.deposit 100.00
    @account.balance.should eq(100.00)
  end

  it "decreases balance by amount of withdrawal" do
    @account.deposit 100.00
    @account.withdraw 10.00
    @account.balance.should eq(90.00)
  end
end

The expected behavior of the withdraw method is that the balance should be decreased by the amount of the withdrawal. Let’s run the test again to confirm that it fails:

$ rspec bankaccount_spec.rb --format documentation

BankAccount
  sets initial balance to zero
  increases balance by amount of deposit
  decreases balance by amount of withdrawal (FAILED - 1)

Failures:

  1) BankAccount decreases balance by amount of withdrawal
     Failure/Error: @account.withdraw 10.00
     NoMethodError:
       undefined method `withdraw' for #<BankAccount:0x007fe459201df0 @balance=100.0>
     # ./bankaccount_spec.rb:20:in `block (2 levels) in <top (required)>'

Finished in 0.00101 seconds
3 examples, 1 failure

Failed examples:

rspec ./bankaccount_spec.rb:18 # BankAccount decreases balance by amount of withdrawal

Now we’ll write code to get this test to pass:

class BankAccount
  def initialize
    @balance = 0.00
  end

  def balance
    @balance
  end

  def deposit (amount)
    @balance += amount
  end

  def withdraw (amount)
    @balance -= amount
  end
end

Now when we run the test again it should pass:

$ rspec bankaccount_spec.rb --format documentation

BankAccount
  sets initial balance to zero
  increases balance by amount of deposit
  decreases balance by amount of withdrawal

Finished in 0.00142 seconds
3 examples, 0 failures

OK, (brushing hands together) it looks like we’re done. But wait, what if there are insufficient funds to cover the withdrawal amount? We should add an example for that also.

Before we do, let’s refactor the test code to incorporate some of the recommendations from Jared Caroll’s article RSpec Best Practices. In that article he recommends wrapping examples for each method in their own describe block with the method’s name as the argument. In addition he recommends prefixing the method name with a “#” for instance methods and a “.” for class methods for clarity. He also recommends using context to explain the scenarios that the method can be tested under. For example, our withdraw method can be executed when there are sufficient funds or when there are insufficient funds in the account.

Here’s how the spec looks with these changes:

require './bankaccount'

describe BankAccount do

  before :each do
    @account = BankAccount.new
  end

  describe "#initialize" do
    it "sets initial balance to zero" do
      @account.balance.should eq(0.00)
    end
  end

  describe "#deposit" do
    it "increases balance by amount of deposit" do
      @account.deposit 100.00
      @account.balance.should eq(100.00)
    end
  end

  describe "#withdraw" do
    context "when sufficient funds" do
      it "decreases balance by amount of withdrawal" do
        @account.deposit 100.00
        @account.withdraw 10.00
        @account.balance.should eq(90.00)
      end
    end
  end

end

Just to be sure that the test still works, let’s run rspec again:

$ rspec bankaccount_spec.rb --format documentation

BankAccount
  #initialize
    sets initial balance to zero
  #deposit
    increases balance by amount of deposit
  #withdraw
    when sufficient funds
      decreases balance by amount of withdrawal

Finished in 0.00134 seconds
3 examples, 0 failures

Oh, that’s a bit more informative! Let’s add our insufficient funds test now:

require './bankaccount'

describe BankAccount do

  before :each do
    @account = BankAccount.new
  end

  describe "#initialize" do
    it "sets initial balance to zero" do
      @account.balance.should eq(0.00)
    end
  end

  describe "#deposit" do
    it "increases balance by amount of deposit" do
      @account.deposit 100.00
      @account.balance.should eq(100.00)
    end
  end

  describe "#withdraw" do
    context "when sufficient funds" do
      it "decreases balance by amount of withdrawal" do
        @account.deposit 100.00
        @account.withdraw 10.00
        @account.balance.should eq(90.00)
      end
    end

    context "when insufficient funds" do
      it "does not decrease balance" do
        @account.deposit 100.00
        @account.withdraw 200.00
        @account.balance.should eq(100.00)
      end
    end
  end

end

Will this test pass?

$ rspec bankaccount_spec.rb --format documentation

BankAccount
  #initialize
    sets initial balance to zero
  #deposit
    increases balance by amount of deposit
  #withdraw
    when sufficient funds
      decreases balance by amount of withdrawal
    when insufficient funds
      does not decrease balance (FAILED - 1)

Failures:

  1) BankAccount#withdraw when insufficient funds does not decrease balance
     Failure/Error: @account.balance.should eq(100.00)

       expected: 100.0
            got: -100.0

       (compared using ==)
     # ./bankaccount_spec.rb:35:in `block (4 levels) in <top (required)>'

Finished in 0.00156 seconds
4 examples, 1 failure

Failed examples:

rspec ./bankaccount_spec.rb:32 # BankAccount#withdraw when insufficient funds does not decrease balance

That’s helpful, RSpec even tells us what the expected and actual values were. Let’s correct this oversight and retest:

class BankAccount
  def initialize
    @balance = 0.00
  end

  def balance
    @balance
  end

  def deposit (amount)
    @balance += amount
  end

  def withdraw (amount)
    if @balance >= amount
      @balance -= amount
    end
  end
end

Now when we run the tests they should all pass:

$ rspec bankaccount_spec.rb --format documentation

BankAccount
  #initialize
    sets initial balance to zero
  #deposit
    increases balance by amount of deposit
  #withdraw
    when sufficient funds
      decreases balance by amount of withdrawal
    when insufficient funds
      does not decrease balance

Finished in 0.00156 seconds
4 examples, 0 failures

Hurray!

Final Thoughts

Clearly I’ve just scratched the surface of what is possible with RSpec. It appears to be a very flexible unit testing tool that also provides useful test result documentation. Thankfully, Michael Hartl has interwoven BDD with RSpec throughout the Ruby on Rails Tutorial since it seems that this will be an invaluable tool for unit testing.

Leave a Comment

Your email address will not be published. Required fields are marked *

Loading...