Tip #16 - Valid Models Don't Have to be Hard
May 12th, 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
May 12th, 2008 at 08:50 PM
I believe you have ==s that should be =s in your first few examples.
May 12th, 2008 at 09:19 PM
Henrik: Yup, thanks, fixed it.
May 13th, 2008 at 08:20 AM
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
May 13th, 2008 at 08:31 AM
Duncan, good point, I changed the code above. Thanks for that.
May 13th, 2008 at 09:47 PM
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 endI like your approach, but this approach kinda keeps everything viewable in the same file (I like that!).
May 13th, 2008 at 09:59 PM
@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:
Which I think is more readable… YMMV…
But I agree with you, it is always good to have the stuff in one file.
Mikel