meetteem: tech addict

software development, etc.

Viewing posts tagged 'rspec'

Factory Girl!

Last month, factory_girl was featured in the RailsEnvy podcast. factory_girl is one of the best things that happened to my Rails TDD/BDD life. It's the only other thing I use with rspec and rspec-rails, though I'm pretty open, so suggestions to make making tests easier are very much welcome. The one con is that factory_girl is tied to ActiveRecord, but it's not really an issue for me because all my non-personal projects use AR. If you are still using fixtures, take a look at factory_girl. It's really nice. Anyway...

factory_girl has a nice feature that lets you define a sequence which you can use to generate unique things. For example, to generate an email address, you can do this (taken from factory_girl's rdoc):

# define a sequence
Factory.sequence :email {|n| "user#{n}@example.com" }

# calling the `next` method
Factory.next :email
# => "user1@example.com"

Factory.next :email
# => "user2@example.com"

However, one of the behaviors in my projects that is not so easy to do with factory_girl is generating unique strings that cannot contain numbers.

I added this behavior in my projects using the following monkey patch I made.

class Factory

  def self.sequence(name, initial_value = 1, &block)
    self.sequences[name] = Sequence.new(initial_value, &block)
  end

  class Sequence

    def initialize(initial_value = 1, &proc)
      @proc = proc
      @value = initial_value
    end

    def next
      prev, @value = @value, @value.next
      @proc.call(prev)
    end

  end

end

This would make the sequence more generic such that anything that has a next method can be passed. A string object has next method so we can use that to generate unique and sequential strings, with no numeric characters. For example,

Factory.sequence :email, 'aaaa' {|s| "user#{s}@example.com"}

# calling the `next` method
Factory.next :email
# => "useraaaa@example.com"

Factory.next :email
# => "useraaab@example.com"

We can even implement a PrimeSequence class and pass it to Factory.sequence to generate a prime number sequence. ;)

Last night, I forked factory_girl in GitHub and applied this monkey patch to the fork. The clone url is git://github.com/teem/factory_girl.git. You can check it out here. I've included specs as well, which has the prime number sequence I mentioned earlier.

Hope this helps you in making the world a better place.

P.S. I recently found Rick's model_stubbing. I haven't looked at it so atm, I have no idea how it works, but the features look promising.

Be Careful When Stubbing

On rare cases, stubbing a reader method might not be the best idea.

To demonstrate this, look at the following example.

Here's a working RSpec spec.

describe Person do
  describe "#has_title?" do
    describe "when it has title" do
      it "should return true" do
        p = Person.new("Tim", "Medina", "Ph.D.")
        p.has_title?.should be_true
      end
    end

    describe "when it has no title" do
      it "should return false" do
        p = Person.new("Tim", "Medina")
        p.has_title?.should be_false
      end
    end
  end
end

And here is the corresponding class (Note that Person#has_title? has been coded in such a way to demonstrate where stubbing fails.):

class Person

  attr_accessor :first_name, :last_name, :title

  def initialize(first_name, last_name, title=nil)
    @first_name = first_name
    @last_name = last_name
    @title = title
  end

  def full_name
    "#{@first_name} #{@last_name}"
  end

  def has_title?
    with_title = @title ? full_name.concat(", #{@title}") : full_name
    full_name.size != with_title.size
  end
end

The tests pass.

But what if we stub Person#full_name?

describe Person do
  describe "#has_title?" do
    describe "when full_name is stubbed" do
      it "should return false" do
        p = Person.new("Tim", "Medina")
        p.stub!(:full_name).and_return("Tim Medina")
        p.has_title?.should be_false
      end

      it "should return true" do
        p = Person.new("Tim", "Medina", "Ph.D.")
        p.stub!(:full_name).and_return("Tim Medina")
        p.has_title?.should be_true
      end
    end
  end
end

Here, the tests fail in the second example.

The culprit: concat is sent to the full_name method which in the example is stubbed. concat modifies the stubbed method full_name, which is really a read-only method and in the next line, full_name has been concatenated with the title.

So be careful when stubbing 'read-only' methods. The implementation might send ! methods and other methods that modify their receivers, like Hash#delete_if, to the methods you're stubbing. RSpec treats stubbed methods as accessor methods. By design, it doesn't care if you implemented the stubbed method as a read-only method.