Monday, August 22, 2011

RSpec, Capybara and your mom

The Shirking

I've always found an excuse to do very minimal or no testing in the rails apps I write. Mostly (so I tell myself) it's because I write them all by my lonesome, so it's faster to just write it than to test it, and I trust my own code (who doesn't).

Recently though, the development team at Shuntyard has been growing and I find myself in need of some peace of mind when it comes to the code base.

The devs are good, or they would not be working with us in the first place. No, the tests are not so much for testing broken code as it is for testing broken functionality. You see, developers like to solve problems in different ways. Often simply on principle. So, when someone "cleverly" refactors a perfectly fine piece of code, we need to know if it breaks something down the line.

Enter RSpec.

I an not too concerned with unit tests or testing controllers. I trust these work well enough to get the job done, if the job gets done. So what I want to test is application behaviour. Can I log in ? Can I change my profile? Do I see a list of things when I click on the right links ? So, what I'm realy after is acceptance testing.

Enter Capybara.

There are quite a few tools out there to use for testing Rails apps. They al have merit (I presume) or they would not exist. For the testing of Wami though, I opted for something that cut close to the core, and did not abstract the testing too much.


I also make use of factory_girl_rails to construct models, instead of the default fixtures.

In a land before time...
So, on to the testing. I decided to start from scratch, so that I can see exactly how it all fit together .

First off, I added the gems for rspec, capybara to my Gemfile. I already had guard in there, having started this quest for fire on railscasts.com, guard seemed like a good tool. So here is the relevant part of my gemfile:

gem 'rspec-rails', :group => [:test, :development]

group :test do
  # Pretty printed test output
  gem 'turn', :require => false
  gem 'capybara'
  gem 'factory_girl_rails'
  gem 'guard-rspec'
end

The next step is to install rspec properly into the app:
$ rails g rspec:install
   identical  .rspec
       exist  spec
      create  spec/spec_helper.rb



Don't forget to wire capybara in, by adding this to your spec_helper:
require 'capybara/rails'

Now, to test that a user can sign up to my site. We use devise for authentication, but that is irrelevant to the test, as I'm testing behaviour only.

To do this, I need to create a spec for signing in.

$ rails g integration_test signup
      invoke  rspec
      create    spec/requests/signups_spec.rb

Automatic, sort of
Ok, here is where the headaches started. Guard, for all it's automated goodness, was not running my migrations when I added fields to models. The errors it gives as a result are also not too untuitive.

For instance, I added a cellnumber to my model, even before I first ran guard.

Failure/Error: user = Factory(:user)
ActiveRecord::UnknownAttributeError:
   unknown attribute: cellnumber
   # ./app/models/user.rb:31:in `new'
   # ./app/models/user.rb:31:in `create_primary_cell'
   # ./spec/requests/sign_ins_spec.rb:5:in `block (2 levels) in '
At some point I had apparently migrated my test database, but before I added this field with a migration.

I eventually figured that the migrations was the problem, when I manually ran a

$ rake spec:requests

and saw the migrations run explicitly there. Starting guard up after that got through most of the tests, at least past the stumbling block.

So, remember to migrate your test DB also!

The ugly...

... part about RSpec is the counter intuitive DSL. I suppose it's hard to get it right, and there is a good deal of helpers around, but I'm yet to see a concise list of valid rspec matchers...

I'll deal with them in separate posts as I discover them.

In the meantime, where are my two tests:
 describe "SignIns" do
  it "signs in a valid user" do
    user = Factory(:user)
    user.confirm!
    user.confirmed_at.should be_within(1.minute).of(Time.now)
  
    visit user_session_path
    fill_in "user_email" , :with => user.email
    fill_in "user_password" , :with => 'secret'
    click_button "Sign in"
    current_path.should eq(root_path)
    page.should have_content("Signed in as #{user.email}")
  end
end

 describe "PasswordResets" do
  it "emails users when requesting password reset" do
    user = Factory(:user)
  
    visit user_session_path
    click_link 'password'
    fill_in "Email" , :with => user.email
    click_button "reset password"
    current_path.should eq(user_session_path)
    page.should have_content("You will receive an email with instructions about how to reset your password in a few minutes")
    last_email.to.should include(user.email)
  end
end