Tip #16 - Valid Models Don't Have to be Hard

Sun May 11 14:38:18 -0700 2008

If you are using BE DE DE or TE DE DE, then you will get situations in your specs or tests where you want to be able to just create a valid model of another type to test against. This is where factories and builders come in handy.

I can’t really say that I can give a dissertation on what exactly makes a factory a FACTORY or a builder a BUILDER, but what I do know is this:

When I am testing a model’s interactions with another class, I like to know that those interactions are being tested against exactly the class that I am using in development. If the class changes and this breaks the associations with the class I am using, then I want that test to fail fast and to show me what is going wrong so I can fix it.

Fixtures in rails help you do this, for example, you can have a fixture:

1
2
3
4
5
6
7
8
# fixtures/users.yml
bob:
  name: bob
  job_id: 1

# fixtures/jobs.yml
party:
  name: Party In Charge

And then do a spec like this:

1
2
3
4
5
6
7
8
9
10
fixtures :users, :jobs

describe "A new job" do
  it "should not be an administrator" do
    @user = users(:bob)
    @job = jobs(:party)
    @user.job = @job
    @user.job.should == @job
  end
end

Which will pass.

but we are not testing if bob is now the Party I/C. We are just testing to see if the user model exhibits the behaviour of having a job if it is allocated one. We picked jobs(:party) because it was there in the YAML file and it looked OK to use at this point of time.

Problem is if you go and modify your users model and decide that any job that has the word ‘Party’ in it would not be valid, then the user spec above would fail… for no good reason.

The problem is with fixtures is that you have to maintain them. And when you have 20 fixtures and all you want is a valid instance of another class, which fixture do you load? Do you want to load all those fixtures just so you could do ”@user = User.new(valid_params)” ? Probably not. What you want is a way just to tell your spec “Make me a new user instance with all the defaults so I can test against it!”

Factories/Builders/Bob the Builder (or whatever you want to call them, I like Bobs) come into play here.

With it you can do something like this:

1
2
3
4
5
6
7
8
describe "A new job" do
  it "should not be an administrator" do
    @user = User.build_valid
    @job = Job.build_valid
    @user.job = @job
    @user.job.should == @job
  end
end

And it will work… hopefully forever… but at least until you change the way your associations work (at which point you would expect it to fail.)

Doing it this way makes a lot of sense. What you do is assign to every class a ‘build_valid’ method and a ‘build_valid!’ method that is guaranteed to return a valid instance of that object, every time.

You then mix these methods into every class you have through the powers of Ruby meta programming, and voila, you can do the above.

Now, this isn’t my idea, I got it from Paul Gross and I am going to rip his code off in the true spirit of open source :) with a modification of my own below. The only real problem I found with Pauls code, is that it can lead to some method name conflicts in your ActiveRecord models, because if you had some class that was called something that is the same name as a method name within Active record (like class Name) then you get ALL sorts of weird errors, we fix this by adding ‘builder_’ to the front of our methods.

So without further ado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# spec/factory.rb
module Factory

  def self.included(base)
    base.extend(self)
  end

  def build_valid(params = {})
    unless self.respond_to?("builder_#{self.name.underscore}")
      raise "There are no default params for #{self.name}"
    end
    new(self.send("builder_#{self.name.underscore}").merge(params))
  end

  def build_valid!(params = {})
    obj = build(params)
    obj.save!
    obj
  end

  def builder_user
    {
      :name => "Bob"
    }
  end

  def builder_job
    {
      :name => 'My Job'
    }
  end
end

ActiveRecord::Base.class_eval do
  include Factory
end

Then in your spec/spec_helper.rb file, somewhere near the top (after all the other requires) put:

1
2
3
4
5
6
7
8
9
10
11
# from the project root directory.
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'spec'
require 'spec/rails'

# Add in the factory
require 'spec/factory'

Spec::Runner.configure do |config|
# Etc etc etc...

Now you can build new classes with ease.

Check out Paul’s blog on this, as he goes into detail on how you use it.

blogLater

Mikel

  1. Henrik N Says:

    I believe you have ==s that should be =s in your first few examples.

  2. Mikel Says:

    Henrik: Yup, thanks, fixed it.

  3. Duncan Beevers Says:

    It might be better to use a method name like build_valid instead of just build.

    This way, you could use the factories on associations without stepping on Rails association build method’s toes.

    User.build_valid

  4. Mikel Says:

    Duncan, good point, I changed the code above. Thanks for that.

  5. Clemens Kofler Says:

    Geoffrey Grosenbach uses another method in his screencasts sometimes.

    In your user_spec.rb, you’d have the following code:
    module UserSpecHelper
      def valid_user_attributes
        {
          :name => "Clemens" 
          # add others here as appropriate
        }
      end
    end
    
    describe User do
      include UserSpecHelper
      # maybe some before method ...
    
      it "should require a name" do
        @user.attributes = valid_user_attributes.except(:name)
        @user.should have_at_least(1).error_on(:name)
      end
    end
    

    I like your approach, but this approach kinda keeps everything viewable in the same file (I like that!).

  6. Mikel Says:

    @Clemens

    I actually use Geoffrey’s approach in all my spec’s in addition to this little builder.

    The idea of the builder is that you can call a valid model from any other spec in the system. So you are doing a functional test in say, memberships and you want a user to grant a membership to, you can go ‘memebership.user = User.build_valid’ and you are done.

    The idea is that you have a certain way of getting a valid object.

    You could always do:

    it "should require a name" do
      @user = User.build_valid(:name => nil)
      @user.should have_at_least(1).error_on(:name)
    end

    Which I think is more readable… YMMV

    But I agree with you, it is always good to have the stuff in one file.

    Mikel

  7. David Chelimsky Says:

    Mikel – have you seen the ObjectDaddy library at http://github.com/flogic/object_daddy? It solves the same problem you’re solving here in a similar way, but with a some additional flexibility built in.

  8. Yossef Says:

    Wow, I was just following a link from ruby-talk and looking through some posts. I didn’t expect to see Object Daddy being plugged.

Leave a Reply