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 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:
- An Introduction to RSpec by David Chelimsky.
- RSpec Best Practices by Jared Carroll.
- Ruby for Newbies: Testing with RSpec by Andrew Burgess.
- Ruby on Rails Tutorial by Michael Hartl.
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:
You should get a response like the following:
You may want to create a directory where for our test files at this point:
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:
Note the “./” preceding the file name for the BankAccount class. This is necessary when referring to a file within the same directory.
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:
The test failed as expected since the file doesn’t exist yet. Let’s remedy this by creating the BankAccount.rb file:
Not very interesting yet, but our test should pass this time. From the shell prompt run the rspec command again:
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:
So, what’s up with “it?”
it turns out to be a method that returns an instance of an
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.
Now we’ll add the code to our BankAccount.rb file to get this test to pass:
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:
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:
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.
Now let’s try to get this test to pass by defining the
deposit method. Edit the BankAccount.rb file to include the
Now when we run RSpec again, the test should pass:
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:
: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:
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:
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:
Now we’ll write code to get this test to pass:
Now when we run the test again it should pass:
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:
Just to be sure that the test still works, let’s run rspec again:
Oh, that’s a bit more informative! Let’s add our insufficient funds test now:
Will this test pass?
That’s helpful, RSpec even tells us what the expected and actual values were. Let’s correct this oversight and retest:
Now when we run the tests they should all pass:
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 *