Behavior-driven development (BDD) is similar to test-driven development (TDD), but the tests for BDD are written in an easier-to-understand language so that developers and clients alike can clearly understand what is being tested. In this article based on chapter 2 of Rails 3 in Action, the authors discuss two tools for BDD: RSpec and Cucumber.
Authors: Yehuda Katz and Ryan A. Bigg
This article is based on “Rails 3 in Action“, published in September 2011. It is being reproduced here by permission from Manning Publications. Manning early access books and ebooks are sold exclusively through Manning. Visit the book’s page for more information.
Behavior-driven development (BDD) is similar to test-driven development (TDD), but the tests for BDD are written in an easier-to-understand language so that developers and clients alike can clearly understand what is being tested. The two tools we cover for BDD are RSpec and Cucumber.
RSpec tests are written in a Ruby domain-specific language (DSL), like this:
describe Bacon do
it "is edible" do
Bacon.edible?.should be_true
end
end
The benefits of writing tests like this are that clients can understand precisely what the test is testing and then use these steps in acceptance testing; a developer can read what the feature should do and then implement it; and, finally, the test can be run as an automated test. With tests written in DSL, you have the three important elements of your business (the clients, the developers, and the code) all operating in the same language.
RSpec is an extension of the methods already provided by Test::Unit. You can even use Test::Unit methods inside of RSpec tests if you wish. But we’re going to use the simpler, easier-to-understand syntax that RSpec provides.
Cucumber tests are written in a language called Gherkin, which goes like this:
Given I am reading a book
When I read this example
Then I should learn something
Each line indicates a step. The benefit of writing tests in the Gherkin language is that it’s closer to English than RSpec is, making it even easier for clients and developers to read.
RSpec
RSpec is a BDD tool written by Steven R. Baker and now maintained by David Chelimsky as a cleaner alternative to Test::Unit, with RSpec being built as an extension to Test::Unit. With RSpec, you write code known as specs that contain examples, which are synonymous to the tests you know from Test::Unit. In this example, you’re going to define the Bacon constant and then define the edible? method on it.
Let’s jump right in and install the rspec gem by running gem install rspec. You should see the following output:
Successfully installed rspec-core-2.6.4
Successfully installed rspec-expectations-2.6.4
Successfully installed rspec-mocks-2.6.4
Successfully installed rspec-2.6.4
You can see that the final line says the rspec gem is installed, with the version number specified after the name. Above this line, you also see a thank-you message and, underneath, the other gems that were installed. These gems are dependencies of the rspec gem, and as such, the rspec gem won’t work without them.
When the gem is installed, create a new directory for your tests called bacon anywhere you like, and inside that, create another directory called spec. If you’re running a UNIX-based operating system such as Linux or Mac OS X, you can run the mkdir -p bacon/spec command to create these two directories. This command will generate a bacon directory if it doesn’t already exist, and then generate in that directory a spec directory.
Inside the spec directory, create a file called bacon_spec.rb. This is the file you use to test your currently nonexistent Bacon class. Put the code from the following listing in spec/bacon_spec.rb.
Listing 1 bacon/spec/bacon_spec.rb
describe Bacon do
it "is edible" do
Bacon.edible?.should be_true
end
end
You describe the (undefined) Bacon class and write an example for it, declaring that Bacon is edible. The describe block contains tests (examples) that describe the behavior of bacon. In this example, whenever you call edible? on Bacon, the result should be true. should serves a similar purpose to assert, which is to assert that its object matches the arguments passed to it. If the outcome is not what you say it should be, then RSpec raises an error and goes no further with that spec.
To run the spec, you run rspec spec in a terminal in the root of your bacon directory. You specify the spec directory as the first argument of this command so RSpec will run all the tests within that directory. This command can also take files as its arguments if you want to run tests only from those files.
When you run this spec, you get an uninitialized constant Object::Bacon error, because you haven’t yet defined your Bacon constant. To define it, create another directory inside your Bacon project folder called lib, and inside this directory, create a file called bacon.rb. This is the file where you define the Bacon constant, a class, as in the following listing.
Listing 2 bacon/lib/bacon.rb
class Bacon
end
You can now require this file in spec/bacon_spec.rb by placing the following line at the top of the file:
require 'bacon'
When you run your spec again, because you told it to load bacon, RSpec has added the lib directory on the same level as the spec directory to Ruby’s load path, and so it will find the lib/bacon.rb for your require. By requiring the lib/bacon.rb file, you ensure the Bacon constant is defined. The next time you run it, you get an undefined method for your new constant:
1) Bacon is edible
Failure/Error: Bacon.edible?.should be_true
NoMethodError:
undefined method `edible?' for Bacon:Class
This means you need to define the edible? method on your Bacon class. Re-open lib/bacon.rb and add this method definition to the class:
def self.edible?
true
end
Now the entire file looks like the following listing.
Listing 3 bacon/lib/bacon.rb
class Bacon
def self.edible?
true
end
end
By defining the method as self.edible?, you define it for the class. If you didn’t prefix the method with self., it would define the method for an instance of the class rather than for the class itself. Running rspec spec now outputs a period, which indicates the test has passed. That’s the first test—done.
For the next test, you want to create many instances of the Bacon class and have the edible? method defined on them. To do this, open lib/bacon.rb and change the edible? class method to an instance method by removing the self. from before the method, as in the next listing.
Listing 4 bacon/lib/bacon.rb
class Bacon
def edible?
true
end
end
When you run rspec spec again, you get the familiar error:
1) Bacon edible?
Failure/Error: its(:edible?) { should be_true }
expected false to be true
Oops! You broke a test! You should be changing the spec to suit your new ideas before changing the code! Let’s reverse the changes made in lib/bacon.rb, as in the following listing.
Listing 5 bacon/lib/bacon.rb
class Bacon
def self.edible?
true
end
end
When you run rspec spec, it passes. Now let’s change the spec first, as in the next listing.
Listing 6 bacon/spec/bacon_spec.rb
describe Bacon do
it "is edible" do
Bacon.new.edible?.should be_true
end
end
In this code, you instantiate a new object of the class rather than using the Bacon class. When you run rspec spec, it breaks once again:
NoMethodError in 'Bacon is edible'
undefined method `edible?' for #
If you remove the self. from the edible? method, your test will now pass, as in the following listing.
Listing 7 Terminal
$ rspec spec
.
1 example, 0 failures
Now you can go about breaking your test once more by adding additional functionality: an expired! method, which will make your bacon inedible. This method sets an instance variable on the Bacon object called @expired to true, and you use it in your edible? method to check the bacon’s status.
First you must test that this expired! method is going to actually do what you think it should do. Create another example in spec/bacon_spec.rb so that the whole file now looks like the following listing.
Listing 8 bacon/spec/bacon_spec.rb
require 'bacon'
describe Bacon do
it "is edible" do
Bacon.new.edible?.should be_true
end
it “expired!” do
bacon = Bacon.new
bacon.expired!
bacon.should be_expired
end
end
When you find you’re repeating yourself, stop! You can see here that you’re defining a bacon variable to Bacon.new and that you’re also using Bacon.new in the first example. You shouldn’t be repeating yourself like that!
A nice way to tidy this up is to move the call to Bacon.new into a subject block. subject calls allow you to create an object to reference in all specs inside the describe block, declaring it the subject (both literally and figuratively) of all the tests inside the describe block. You can define a subject like this:
subject { Bacon.new }
In the context of the entire spec, it looks like the following listing.
Listing 9 bacon/spec/bacon_spec.rb
require 'bacon'
describe Bacon do
subject { Bacon.new }
it “is edible” do
Bacon.new.edible?.should be_true
end
it “expired!” do
bacon = Bacon.new
bacon.expired!
bacon.expired.should be_true
end
end
Now that you have the subject, you can cut a lot of the code out of the first spec and refine it:
its(:edible?) { should be_true }
First, the its method takes the name of a method to call on the subject of these tests. The block specified should contain an assertion for the output of that method. Unlike before, you’re not calling should on an object, as you have done in previous tests, but rather on seemingly nothing at all. If you do this, RSpec figures out that you mean the subject you defined, so it calls should on that.
You can also reference the subject manually in your tests, as you’ll see when you write the expired! example shown in the following listing.
Listing 10 bacon/spec/bacon_spec.rb
it "expired!" do
subject.expired!
subject.should_not be_edible
end
Here, the expired! method must be called on the subject because it is only defined on your Bacon class. For readability’s sake, you explicitly call the should_not method on the subject and specify that edible? should return false.
If you run rspec spec again, your first spec still passes, but your second one fails because you have yet to define your expired! method. Let’s do that now in lib/bacon.rb, as shown in the following listing.
Listing 11 bacon/lib/bacon.rb
class Bacon
def edible?
true
end
def expired!
self.expired = true
end
end
By running rspec spec again, you get an undefined method error:
NoMethodError in 'Bacon expired!'
undefined method `expired=' for #
This method is called by the following line in the previous example:
self.expired = true
To define this method, you can use the attr_accessor method provided by Ruby, as shown in listing 12; the attr prefix of the method means attribute. If you pass a Symbol (or collection of symbols) to this method, it defines methods for setting (expired=) and retrieving the attribute expired values, referred to as a setter and a getter, respectively. It also defines an instance variable called @expired on every object of this class to store the value that was specified by the expired= method calls.
WARNING In Ruby you can call methods without the self. prefix. You specify the prefix because otherwise the interpreter will think that you’re defining a local variable. The rule for setter methods is that you should always use the prefix.
Listing 12 bacon/lib/bacon.rb
class Bacon
attr_accessor :expired
...
end
With this in place, if you run rspec spec again, your example fails on the line following your previous failure:
Failure/Error: subject.should_not be_edible
expected edible? to return false, got true
Even though this sets the expired attribute on the Bacon object, you’ve still hardcoded true in your edible? method. Now change the method to use the attribute method, as in the following listing.
Listing 13 bacon/lib/bacon.rb
def edible?
!expired
end
When you run rspec spec again, both your specs will pass:
..
2 examples, 0 failures
Let’s go back in to lib/bacon.rb and remove the self. from the expired! method:
def expired!
expired = true
end
If you run rspec spec again, you’ll see your second spec is now broken:
Failure/Error: Bacon expired!
expected edible? to return false, got true
Tests save you from making mistakes such as this. If you write the test first and then write the code to make the test pass, you have a solid base and can refactor the code to be clearer or smaller and finally ensure that it’s still working with the test you wrote in the first place. If the test still passes, then you’re probably doing it right.
If you change this method back now
def expired!
self.expired = true
end
and then run your specs using rspec spec, you’ll see that they once again pass:
..
2 examples, 0 failures
Everything’s normal and working once again, which is great!
That ends our little foray into RSpec for now. You’ll use it again later when you develop your application. If you’d like to know more about RSpec, The RSpec Book: Behavior-Driven Development with RSpec, Cucumber, and Friends (David Chelimsky et al., Pragmatic Bookshelf, 2010) is recommended reading.
Cucumber
For this section, we retire the Bacon example and go for something more formal with Cucumber.
NOTE This section assumes you have RSpec installed. If you don’t, use this command to install it: gem install rspec.
While RSpec and Test::Unit are great for unit testing (testing a single part), Cucumber is mostly used for testing the entire integration stack.
Cucumber’s history is intertwined with RSpec, so the two are similar. In the beginning of BDD, as you know, there was RSpec. Shortly thereafter, there were RSpec Stories, which looked like the following listing.
Listing 14 Example
Scenario "savings account is in credit" do
Given "my savings account balance is", 100 do |balance|
@account = Account.new(balance)
end
...
end
The idea behind RSpec Stories is that they are code- and human-readable stories that can be used for automated testing as well as quality assurance (QA) testing by stakeholders. Aslak Hellesoy rewrote RSpec Stories during October 2008 into what we know today as Cucumber. The syntax remains similar, as seen in the following listing.
Listing 15 Example
Scenario: Taking out money
Given I have an account
And my account balance is 100
When I take out 10
Then my account balance should be 90
What you see here is known as a scenario in Cucumber. Under the scenario’s title, the remainder of the lines are called steps, which are read by Cucumber’s parser and matched to step definitions, where the logic is kept. Scenarios are found inside a feature, which is a collection of common scenarios. For example, you may have one feature for dictating what happens when a user creates projects and another for when a user creates tickets.
Notice the keywords Given, And, When, and Then. These are just some of the keywords that Cucumber looks for to indicate that a line is a step. If you’re going to be using the same keyword on a new line, it’s best practice to instead use the And keyword because it reads better. Try reading the first two lines aloud from the previous listing, then replace the And with Given and try it again. It just sounds right to have an And there rather than a Given.
Given steps are used for setting up the scene for the scenario. This example sets up an account and gives it a balance of 100.
When steps are used for defining actions that should happen during the scenario. The example says, When I take out 10.
Then steps are used for declaring things that take place after the When steps have completed. This example says, When I take out 10 Then my account balance should be 90.
These different step types add a great deal of human readability to the scenarios in which they’re used, even though they can be used interchangeably. You could define all the steps as Givens, but it’s not really readable. Let’s now implement this example scenario in Cucumber. First, run mkdir -p accounts/features, which, much as in the RSpec example, creates a directory called accounts and a directory inside of that called features. In this features directory, create a file called account.feature. In this file, you write a feature definition, as shown in the following listing.
Listing 16 accounts/features/account.feature
Feature: My Account
In order to manage my account
As a money minder
I want to ensure my money doesn't get lost
This listing lays out what this feature is about and is more useful to human readers (such as stakeholders) than it is to Cucumber.
Next, you put in your scenario underneath the feature definition, as in the following listing.
Listing 17 accounts/features/account.feature
Scenario: Taking out money
Given I have an account
And it has a balance of 100
When I take out 10
Then my balance should be 90
The whole feature should now look like the following listing.
Listing 18 accounts/features/account.feature
Feature: My Account
In order to manage my account
As a money minder
I want to ensure my money doesn't get lost
Scenario: Taking out money
Given I have an account
And it has a balance of 100
When I take out 10
Then my balance should be 90
As you can see in listing 2.23, it’s testing the whole stack of the transaction rather than a single unit. This process is called integration testing. You set the stage by using the Given steps, play out the scenario using When steps, and ensure that the outcome is as you expected by using Then steps. The And word is used when you want a step to be defined in the same way as the previous step, as seen in the first two lines of this scenario.
To run this file, you first need to ensure Cucumber is installed, which you can do by installing the Cucumber gem: gem install cucumber. When the Cucumber gem is installed, you can run this feature file by going into the accounts directory and running cucumber features, as in the next listing.
Listing 19 Terminal
Feature: My Account
In order to manage my account
As a money minder
I want to ensure my money doesn't get lost
Scenario: Taking out money
Given I have an account
And it has a balance of 100
When I take out 10
Then my balance should be 90
This output appears in color in the terminal with the steps of the scenario in yellow, followed by a summary of this Cucumber run, and then a list of what code you used to define the missing steps (not shown in this example output), again in yellow. What this output doesn’t tell you is where to put the step definitions.
All these step definitions should go into a new file located at features/step_definitions/account_steps.rb. The file is called account_steps.rb and not account.rb to clearly separate it from any other Ruby files, so when looking for the steps file, you don’t get it confused with any other file. In this file, you can copy and paste in the steps Cucumber gave you, as in the following listing.
Listing 20 features/step_definitions/account_steps.rb
Given /^I have an account$/ do
pending # express the regexp above with the code you wish you had
end
Given /^it has a balance of (\d+)$/ do |arg1|
pending # express the regexp above with the code you wish you had
end
When /^I take out (\d+)$/ do |arg1|
pending # express the regexp above with the code you wish you had
end
Then /^my balance should be (\d+)$/ do |arg1|
pending # express the regexp above with the code you wish you had
end
If you run cucumber features again, you’ll see that all your steps are defined but not run (signaled by their blue coloring) except the very first one, which is yellow because it’s pending. Now you’re going to restructure the first step to make it not pending. It will now instantiate an Account object that you’ll use for the rest of this scenario.
Listing 21 features/step_definitions/account_steps.rb
Given /^I have an account$/ do
@account = Account.new
end
Steps are defined by using regular expressions, which are used when you wish to match strings. In this case, you’re matching the step in the feature with this step definition by putting the text after the Given keyword into a regular expression. After the regular expression is the do Ruby keyword, which matches up with the end at the end. This syntax indicates a block, and this block is run (“called”) when this step definition is matched.
With this step defined, you can try running cucumber features/account.feature to see if the feature will pass. No—it fails with this error:
Given I have an account
uninitialized constant Object::Account (NameError)
Similar to the beginning of the RSpec showcase, create a lib directory inside your accounts directory. To define this constant, you create a file in this new directory called account.rb. In this file you put code to define the class, shown in the following listing.
Listing 22 accounts/lib/account.rb
class Account
end
This file is not loaded automatically, of course: you have to require it just as you did in RSpec with lib/bacon.rb. Cucumber’s authors already thought of this and came up with a solution. Any file inside of features/support is automatically loaded, with one special file being loaded before all the others: features/support/env.rb. This file should be responsible for setting up the foundations for your tests. Now create features/support/env.rb and put these lines inside it:
$LOAD_PATH << File.expand_path('../../../lib', __FILE__)
require 'account'
When you run this feature again, the first step passes and the second one is pending:
Scenario: Taking out money
Given I have an account
And it has a balance of 100
TODO (Cucumber::Pending)
Go back into features/step_definitions/account_steps.rb now and change the second step’s code to set the balance on your @account object, as shown in the next listing. Note in this listing that you change the block argument from arg1 to amount.
Listing 23 features/step_definitions/account_steps.rb
Given /^it has a balance of (\d+)$/ do |amount|
@account.balance = amount
end
With this step, you’ve used a capture group inside the regular expression. The capture group captures whatever it matches. In Cucumber, the match is returned as a variable, which is then used in the block. An important thing to remember is that these variables are always String objects.
When you run this feature again, this step fails because you haven’t yet defined the method on the Account class:
And it has a balance of 100
undefined method `balance=' for # (NoMethodError)
To define this method, open lib/account.rb and change the code in this file to look exactly like the following listing.
Listing 24 accounts/lib/account.rb
class Account
def balance=(amount)
@balance = amount
end
end
The method is defined as balance=. In Ruby, these methods are called setter methods and, just as their name suggests, they’re used for setting things. Setter methods are defined without the space between the method name and the = sign, but they can be called with or without the space, like this:
@account.balance=100
# or
@account.balance = 100
The object after the equals sign is passed in as the single argument for this method. In this method, you set the @balance instance variable to that value. Now when you run your feature, this step passes and the third one is the pending one:
Scenario: Taking out money
Given I have an account
And it has a balance of 100
When I take out 10
TODO (Cucumber::Pending)
Go back into features/step_definitions/account_steps.rb and change the third step to take some money from your account:
When /^I take out (\d+)$/ do |amount|
@account.balance = @account.balance - amount
End
Now when you run this feature, it’ll tell you there’s an undefined method balance, but didn’t you just define that?
When I take out 10
undefined method `balance' for # (NoMethodError)
Actually, the method you defined was balance= (with an equals sign), which is a setter method. balance (without an equals sign) in this example is a getter method, which is used for retrieving the variable you set in the setter method. Not all methods without equal signs are getter methods, however. To define this method, switch back into lib/account.rb and add this new method directly under the setter method, as shown in the following listing.
Listing 25 accounts/lib/account.rb
def balance=(amount)
@balance = amount
end
def balance
@balance
end
Here you define the balance= and balance methods you need. The first method is a setter method, which is used to set the @balance instance variable on an Account object to a specified value. The balance method returns that specific balance. When you run this feature again, you’ll see a new error:
When I take out 10
String can't be coerced into Fixnum (TypeError)
./features/step_definitions/account_steps.rb:10:in `-'
This error occurred because you’re not storing the balance as a Fixnum but as a String. As mentioned earlier, the variable returned from the capture group for the second step definition is a String. To fix this, you coerce the object into a Fixnum by calling to_i inside the setter method, as shown in the following listing.
Listing 26 accounts/lib/account.rb
def balance=(amount)
@balance = amount.to_i
end
Now anything passed to the balance= method will be coerced into an integer. You also want to ensure that the other value is also a Fixnum. To do this, open features/step_definitions/account_steps.rb and change the third step to look exactly like the following listing.
Listing 27 features/step_definitions/account_steps.rb
When /^I take out (\d+)$/ do |amount|
@account.balance -= amount.to_i
end
That makes this third step pass, because you’re subtracting a Fixnum from a Fixnum. When you run this feature, you’ll see that this step is definitely passing and the final step is now pending:
Scenario: Taking out money
Given I have an account
And it has a balance of 100
When I take out 10
Then my balance should be 90
TODO (Cucumber::Pending)
This final step asserts that your account balance is now 90. You can implement it in features/step_definitions/account_steps.rb, as shown in the following listing.
Listing 28 features/step_definitions/account_steps.rb
Then /^my balance should be (\d+)$/ do |amount|
@account.balance.should eql(amount.to_i)
end
Here you must coerce the amount variable into a Fixnum again so you’re comparing a Fixnum with a Fixnum. With this fourth and final step implemented, your entire scenario (which also means your entire feature) passes:
Scenario: Taking out money
Given I have an account
And it has a balance of 100
When I take out 10
Then my balance should be 90
1 scenario (1 passed)
4 steps (4 passed)
As you can see from this example, Cucumber allows you to write tests for your code in syntax that can be understood by developers, clients, and parsers alike. You’ll see a lot more of Cucumber when you use it in building your next Ruby on Rails application.
Summary
This article demonstrated how to apply BDD principles to test some rudimentary code. You can (and should!) apply these principles to all code you write, because testing the code ensures it’s maintainable from now into the future. You don’t have to use the gems shown in this article to test your Rails application; they are just preferred by a large portion of the community.
Wouldn’t your first rspec example be better expressed as
Bacon.new.should be_edible
as you did with
expired?
to be consistent?