Advertisement

How I Test

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

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.

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 "user@example.com"
  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 "user@example.com" # 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 "user@example.com"'

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 the User 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.

Advertisement