In a recent discussion on Google+, a friend of mine commented, "Test-Driven Development (TDD) and Behavior-Driven Development (BDD) is Ivory Tower BS." This prompted me to think about my first project, how I felt the same way then, and how I feel about it now. Since that first project, I've developed a rhythm of TDD/BDD that not only works for me, but for the client as well.
Ruby on Rails ships with a test suite, called Test Unit, but many developers prefer to use RSpec, Cucumber, or some combination of the two. Personally, I prefer the latter, using a combination of both.
RSpec
From the RSpec site:
RSpec is a testing tool for the Ruby programming language. Born under the banner of Behaviour-Driven Development, it is designed to make Test-Driven Development a productive and enjoyable experience.
RSpec provides a powerful DSL that is useful for both unit and integration testing. While I have used RSpec for writing integration tests, I prefer to use it only in a unit testing capacity. Therefore, I will cover how I use RSpec exclusively for unit testing. I recommend reading The RSpec Book by David Chelimsky and others for complete and in-depth RSpec coverage.
Cucumber
I've found the benefits of TDD/BDD far outweigh the cons.
Cucumber is an integration and acceptance testing framework that supports Ruby, Java, .NET, Flex, and a host of other web languages and frameworks. Its true power comes from its DSL; not only is it available in plain English, but it has been translated into over forty spoken languages.
With a human-readable acceptance test, you can have the customer sign off on a feature, before writing a single line of code. As with RSpec, I will only be covering Cucumber in the capacity in which I use it. For the complete rundown on Cucumber, check out The Cucumber Book.
The Setup
Let's first begin a new project, instructing Rails to skip Test Unit. Type the following into a terminal:
rails new how_i_test -T
Within the Gemfile
, add:
source 'https://rubygems.org' ... group :test do gem 'capybara' gem 'cucumber-rails', require: false gem 'database cleaner' gem 'factory_girl_rails' gem 'shoulda' end group :development, :test do gem 'rspec-rails' end
I mostly use RSpec to ensure that my models and their methods stay in check.
Here, we've put Cucumber and friends inside of the group test
block. This ensures that they are properly loaded only in the Rails test environment. Notice how we also load RSpec inside of the development
and test
blocks, making it available in both environments. There are a few other gems. which I will briefly detail below. Don't forget to run bundle install
to install them.
- Capybara: simulates browser interactions.
- Database Cleaner: cleans the database between test runs.
- Factory Girl Rails: fixture replacement.
- Shoulda: helper methods and matchers for RSpec.
We need to run these gems' generators to set them up. You can do that with the following terminal commands:
rails g rspec:install create .rspec create spec create spec/spec_helper.rb rails g cucumber:install create config/cucumber.yml create script/cucumber chmod script/cucumber create features/step_definitions create features/support create features/support/env.rb exist lib/tasks create lib/tasks/cucumber.rake gsub config/database.yml gsub config/database.yml force config/database.yml
At this point, we could begin writing specs and cukes to test our application, but we can set up a few things to make testing easier. Let's start in the application.rb
file.
module HowITest class Application < Rails::Application config.generators do |g| g.view_specs false g.helper_specs false g.test_framework :rspec, :fixture => true g.fixture_replacement :factory_girl, :dir => 'spec/factories' end ... end end
Inside the Application class, we override a few of Rails' default generators. For the first two, we skip the views and helpers generation specs.
These tests are not necessary, because we are only using RSpec for unit tests.
The third line informs Rails that we intend to use RSpec as our test framework of choice, and it should also generate fixtures when generating models. The final line ensures that we use factory_girl for our fixtures, of which are created in the spec/factories
directory.
Our First Feature
To keep things simple, we're going to write a simple feature for signing into our application. For the sake of brevity, I will skip the actual implementation and stick with the testing suite. Here is the contents of features/signing_in.feature
:
Feature: Signing In In order to use the application As a registered user I want to sign in through a form Scenario: Signing in through the form Given there is a registered user with email "[email protected]" And I am on the sign in page When I enter correct credentials And I press the sign in button Then the flash message should be "Signed in successfully."
When we run this in the terminal with cucumber features/signing_in.feature
, we see a lot of output ending with our undefined steps:
Given /^there is a registered user with email "(.*?)"$/ do |arg1| pending # express the regexp above with the code you wish you had end Given /^I am on the sign in page$/ do pending # express the regexp above with the code you wish you had end When /^I enter correct credentials$/ do pending # express the regexp above with the code you wish you had end When /^I press the sign in button$/ do pending # express the regexp above with the code you wish you had end Then /^the flash message should be "(.*?)"$/ do |arg1| pending # express the regexp above with the code you wish you had end
The next step is to define what we expect each of these steps to do. We express this in features/stepdefinitions/signin_steps.rb
, using plain Ruby with Capybara and CSS selectors.
Given /^there is a registered user with email "(.*?)"$/ do |email| @user = FactoryGirl.create(:user, email: email) end Given /^I am on the sign in page$/ do visit sign_in_path end When /^I enter correct credentials$/ do fillin "Email", with: @user.email fillin "Password", with: @user.password end When /^I press the sign in button$/ do click_button "Sign in" end Then /^the flash message should be "(.*?)"$/ do |text| within(".flash") do page.should have_content text end end
Within each of the Given
, When
, and Then
blocks, we use the Capybara DSL to define what we expect from each block (except in the first one). In the first given block, we tell factory_girl to create a user stored in the user
instance variable for later use. If you run cucumber features/signing_in.feature
again, you should see something similar to the following:
Scenario: Signing in through the form # features/signing_in.feature:6 Given there is a registered user with email "[email protected]" # features/step_definitions/signing\_in\_steps.rb:1 Factory not registered: user (ArgumentError) ./features/step_definitions/signing\_in\_steps.rb:2:in `/^there is a registered user with email "(.*?)"$/' features/signing_in.feature:7:in `Given there is a registered user with email "[email protected]"'
We can see from the error message that our example fails on line 1 with an ArgumentError
of the user factory not being registered. We could create this factory ourselves, but some of the magic we setup earlier will make Rails do that for us. When we generate our user model, we get the user factory for free.
rails g model user email:string password:string invoke active_record create db/migrate/20121218044026\_create\_users.rb create app/models/user.rb invoke rspec create spec/models/user_spec.rb invoke factory_girl create spec/factories/users.rb
As you can see, the model generator invokes factory_girl and creates the following file:
ruby spec/factories/users.rb FactoryGirl.define do factory :user do email "MyString" password "MyString" end end
I won't go into great depth of factory_girl here, but you can read more in their getting started guide. Don't forget to run rake db:migrate
and rake db:test:prepare
to load the new schema. This should get the first step of our feature to pass, and start you down the road of using Cucumber for your integration testing. On each pass of your features, Cucumber will guide you to the pieces that it sees missing to make it pass.
Model Testing with RSpec and Shoulda
I mostly use RSpec to make sure that my models and their methods stay in check. I often also use it for some high level controller testing, but that goes into more detail than this guide allows for. We're going to use the same user model that we previously set up with our sign-in feature. Looking back at the output from running the model generator, we can see that we also got user_spec.rb
for free. If we run rspec spec/models/user_spec.rb
we should see the following output.
Pending: User add some examples to (or delete) /Users/janders/workspace/how\_i\_test/spec/models/user_spec.rb
And if we open that file, we see:
require 'spechelper' describe User do pending "add some examples to (or delete) #{FILE}" end
The pending line gives us the output we saw in the terminal. We'll leverage Shoulda's ActiveRecord and ActiveModel matchers to ensure our user model matches our business logic.
require 'spechelper' describe User do context "#fields" do it { should respondto(:email) } it { should respondto(:password) } it { should respondto(:firstname) } it { should respondto(:lastname) } end context "#validations" do it { should validate_presence_of(:email) } it { should validate_presence_of(:password) } it { should validate_uniqueness_of(:email) } end context "#associations" do it { should have_many(:tasks) } end describe "#methods" do let!(:user) { FactoryGirl.create(:user) } it "name should return the users name" do user.name.should eql "Testy McTesterson" end end end
We setup a few context blocks inside of our first describe
block to test things like fields, validations, and associations. While there are not functional differences between a describe
and a context
block, there is a contextual one. We use describe
blocks to set the state of what we are testing, and context
blocks to group those tests. This makes our tests more readable and maintainable in the long run.
The first
describe
allows us to test against theUser
model in an unmodified state.
We use this unmodified state to test against the database with the Shoulda matchers grouping each by type. The next describe
block sets up a user from our previously created user
factory. Setting up the user with the let
method inside of this block allows us to test an instance of our user model against known attributes.
Now, when we run rspec spec/models/user_spec.rb
, we see that all of our new tests fail.
Failures: 1) User#methods name should return the users name Failure/Error: user.name.should eql "Testy McTesterson" NoMethodError: undefined method name' for #<User:0x007ff1d2775170> # ./spec/models/user_spec.rb:26:in</code>block (3 levels) in <top (required)>' 2) User#validations Failure/Error: it { should validate_uniqueness_of(:email) } Expected errors to include "has already been taken" when email is set to "arbitrary<em>string", got no errors # ./spec/models/user</em>spec.rb:15:in `block (3 levels) in <top (required)>' 3) User#validations Failure/Error: it { should validate_presence_of(:password) } Expected errors to include "can't be blank" when password is set to nil, got no errors # ./spec/models/user_spec.rb:14:in `block (3 levels) in <top (required)>' 4) User#validations Failure/Error: it { should validate_presence_of(:email) } Expected errors to include "can't be blank" when email is set to nil, got no errors # ./spec/models/user_spec.rb:13:in `block (3 levels) in <top (required)>' 5) User#associations Failure/Error: it { should have<em>many(:tasks) } Expected User to have a has</em>many association called tasks (no association called tasks) # ./spec/models/user_spec.rb:19:in `block (3 levels) in <top (required)>' 6) User#fields Failure/Error: it { should respond<em>to(:last</em>name) } expected #<User id: nil, email: nil, password: nil, created_at: nil, updated_at: nil> to respond to :last<em>name # ./spec/models/user</em>spec.rb:9:in `block (3 levels) in <top (required)>' 7) User#fields Failure/Error: it { should respond<em>to(:first</em>name) } expected #<User id: nil, email: nil, password: nil, created_at: nil, updated_at: nil> to respond to :first<em>name # ./spec/models/user</em>spec.rb:8:in <code>block (3 levels) in <top (required)>'
With each of these tests failing, we have the framework we need to add migrations, methods, associations, and validations to our models. As our application evolves, models expand and our schema changes, this level of testing provides us with protection for introducing breaking changes.
Conclusion
While we didn't cover too many topics in depth, you should now have a basic understanding of integration and unit testing with Cucumber and RSpec. TDD/BDD is one of the things developers either seem to do or don't do, but I've found that the benefits of TDD/BDD far outweigh the cons on more than one occasion.
Comments