Controllers, rSpec and Setter Methods

Recently I started working on my latest (secret) project. It will be the biggest website I have ever tried to build, and I will be doing it from scratch using Ruby on Rails. From the start it was clear that I would need to be doing some sort of testing during this project to ensure that the quality of the site was consistent.

I think I first heard about rSpec on the Ruby on Rails Podcast, and was impressed by the expressive syntax and the totally readable specifications you could write.

One last note before you get into the examples, I changed the names of all the classes and variables in this to disguise the nature of my project, so never fear, I am not making a website about Pets, their Owners or Lemons!

The Target Action

In order for this example to work, I first need to show you what the target controller action looks like, it’s not very BDD to do it this way round, but will help this example make sense.

class OwnersController < ApplicationController
  def new
    @owner = Owner.find_by_url(params[:owner])
    @pet = Pet.new
    @pet.owner_id = @owner.id
  end
end

So, when the new action is called, first the Owner is found based on params[:owner] which is set in the URL, then a new Pet is created and finally the owner_id is set to the ID of the current Owner.

Pretty simple stuff really, but as a complete newbie to all of this the third line @pet.owner_id = @owner.id was causing me a real problem when writing my spec for the controller.

An Empty Spec

We know what we are aiming for, so now let’s take a look at the spec for this controller action. I have trimmed out all the other examples to keep this as concise as I can.

describe PetsController, 'handling GET /owner/joe/new-pet' do
  before do
    @pet = mock_model(Pet)
    @owner = mock_model(Owner)
  end

  def do_get
    get :new
  end

  #other examples go here...

  it 'should set @pet.owner_id to the id of the current owner'
end

Take a look at that single example, it has currently not been implemented (leave off the do end) but describes exactly what that last line of the controller action should be doing.

I spent a really long (and fairly frustrating) time trying to work out exactly how to write this example correctly. I was stubbing methods all over the place but nothing was working, until I took a break and something crucial finally dawned on me…

Back to Basics: Ruby Setter Methods

In Ruby, you write attributes using a special method called a Setter Method, consider the following example.

class Lemon
  def bitterness=(new_bitterness)
    @bitterness = new_bitterness
  end
end

lemon = Lemon.new
lemon.bitterness = 200 #it's a really bitter lemon!
lemon.bitterness #outputs 200

There are two things to note here, the first is the way that saying lemon.bitterness = 'something' actually calls a method of the Lemon class. The second is the name of this method, notice the = on the end. With this new knowledge we can now write a passing example.

Completed rSpec Example

Here is the finished specification.

describe PetsController, 'handling GET /owner/joe/new-pet' do
  before do
    @pet = mock_model(Pet)
    @owner = mock_model(Owner)
    @pet.stub!(:owner_id=).and_return(@owner.id) #added
  end

  def do_get
    get :new
  end

  #other examples go here...

  it 'should set @pet.owner_id to the id of the current owner' do
    @pet.should_receive(:owner_id=).with(@owner.id).and_return(@owner.id)
    do_get
  end
end

In the before block we set up a new stub on the @pet instance variable. Notice the equals sign on the end of the name of this stubbed method?

Now we can write the example, in the end one line of code gets the job done. We make sure that that @pet should receive a call to :owner_id= (which translates to @pet.owner_id = 'something') with the current owner id and return the owner id.

Run your specs and it should pass. So that is how you write specs to ensure attributes are being set correctly. Hope it saves someone a headache!

A finally note, I am not an expert at any of this, so as such, there could be a much more efficient way of doing this, so if you are a bit more experienced then please leave a comment and help myself and others improve our knowledge.


Post Meta

Comments

Comments make the world go round, where would we be with out good discussion? Although free speech is encouraged, please make sure you keep on topic. Any abusive posts or spam will be dealt with accordingly. Speak to people how you would like to be spoken to!

  1. Steve

    August 27th, 2007 at 2:36 am

    I know it ruins the point of the post, but your “new” action should look like:

    def new
    @owner = Owner.find_by_url(params[:owner])
    @pet = @owner.pets.create()
    end

    (this assumes that :owner has_many :pets in the model)

  2. Andy Pearson

    Gravatar

    August 27th, 2007 at 10:55 am

    Hey Steve,

    Thanks for the comment, you may have ruined the point of the post, but you have opened my eyes to a much better way of doing this!

    I ended up using:

    @pet = @owner.pets.build

    as create() was validating the model fields on creation.

    It works a treat now, and the spec reads much nicer! Thanks for taking the time to leave a comment and help improve my Rails skills!

Leave a Reply

(required)

(will not be published) (required)