Dave Schweisguth in a Bottle

How many meanings of that can you think of?

Homebrewed ActiveRecord test object factories for rspec

leave a comment »

I recently took GWW from a handful of Test::Unit tests to 100% test coverage. It took some good tools (rspec and RR (Double Ruby) are both terrific), but what I didn’t do was use either of the widely used test object factory libraries, factory_girl or Machinist. I wasn’t going to use Rails fixtures, either — fixtures are just wrong — but the factory libraries rubbed me the wrong way. I could point to bits of each tool that weren’t exactly what I wanted, but the real reason I didn’t use either one was that they both seemed a little much when all I wanted to do was construct a few objects. Yes, I recognized the sound of a wheel about to be reinvented, but I went ahead anyway, and I ended up with a small amount of code very well tuned to my needs, and a couple of nice features that I don’t think the off-the-shelf solutions provide.

First, the requirements:

  • Test objects in model specs should always be saved in the database, since dealing with the database is most of what they do. In other specs (helper, controller, view, routing) objects should never be saved, since the model methods that would need objects to be in the database are always stubbed or mocked and saving is slow. Object factories should create objects appropriate to the type of spec they’re called from.
  • All attributes should have defaults that are easily readable in printed output and in a debugger, and when possible say what object’s attribute and what attribute they’re a value of.
  • It should be easy to make multiple instances of a class without violating uniqueness constraints.
  • It should be easy to specify the value of any attribute at creation time.
  • If an object isn’t saved in the database, it should be possible to specify its ID in the statement that creates it, just like any other attribute. (URLs in view tests look a lot better with non-nil IDs.) ActiveRecord normally prevents one from constructing an object with an ID, so a little extra help is needed here.

ActiveRecord already provides succinct ways to construct an object in memory (.new) or in the database (.create!), so all that’s needed is a little mixin for some syntactic sugar:

module ModelFactory
  def make(label = '', caller_attrs = {})
    if label.is_a? Hash
      caller_attrs = label
      label = ''
    end
    caller_attrs = caller_attrs.clone

    method = caller.find { |line| line =~ /\/spec\/models\// }.nil? ? :new : :create!

    id = caller_attrs[:id]
    if id
      if method != :new
        raise ArgumentError,
          "Can't specify :id for an object which is to be create!d in the database"
      end
      caller_attrs.delete :id
    else
      if method == :new
        id = 0
      end
    end

    attrs = attrs method, label, caller_attrs
    attrs.merge! caller_attrs

    instance = send method, attrs

    if id
      instance.id = id
    end

    instance
  end

  def affix(label, value)
    label = label.to_s
    label.empty? ? value : label + '_' + value
  end

end


Here’s an example of a class that extends the mixin and implements the attrs method to tell the module the default attributes with which the object should be created. It takes only a touch more effort than describing the object in a DSL, and being real code it’s much more flexible:

class Photo
  extend ModelFactory

  def self.attrs(method, label, caller_attrs)
    now = Time.now
    attrs = {
      :flickrid => affix(label, 'photo_flickrid'),
      :farm => '0',
      :server => 'server',
      :secret => 'secret',
      :dateadded => now,
      :lastupdate => now,
      :seen_at => now,
      :game_status => 'unfound',
    }
    if ! caller_attrs[:person]
      attrs[:person] = Person.make affix(label, 'poster')
    end
    attrs
  end

end


The default usage is the same as Machinist’s nice syntax:

person = Photo.make


There is only one .make method, however. It examines the call stack to see if this is a model spec or not and constructs the object using .create! if it is and .new if it is not. (Incidentally, isn’t the repeated punctuation in .nil? ? :new : :create! amusingly hard to write and read?) This is a big win; in an earlier version with separate .make and .make! methods I often caught myself using the wrong one for the type of spec at hand.

Specifying custom values works like you’d expect as well:

photo = Photo.make :flickrid => '8675309'


To make a couple of photos with unique attributes, just give each a different label:

photo1 = Photo.make 1 # :flickrid => '1_photo_flickrid'
photo2 = Photo.make 2 # :flickrid => '2_photo_flickrid'


(I gave my Photos only one labeled field, but of course you can label any field you like.) Note the argument juggling which lets you specify a label, explicit attribute values or both. Note also that Photo’s implementaton of .attrs gives the Photo a labeled Person. Labels accumulate, so this scales up nicely to more complex objects, for example a Guess which has a Person (the guesser) and a Photo which has a different Person (the poster).

Finally, .make also provides a default non-nil ID for objects not saved in the database and allows the user to override it. Both of those things just mean setting the ID immediately after creating the object with .new, since ActiveRecord ignores an ID set in .new. If the object is to be saved in the database, we stay out of ActiveRecord’s way and let it fill the ID on saving.

No rocket surgery here, just YAGNI and relentless refactoring. There’s nothing here I don’t need, and everything I do.

Update, three years later:

You knew the good times couldn’t last. I eventually decided that ModelFactory‘s feature of using a label provided by the test writer is not useful often enough– I’m more likely to debug a test failure from my knowledge of what I just changed than by reading object attributes — and having to specify the label to prevent uniqueness constraint violations is annoying. factory_girl’s standard approach of differentiating attributes with sequences is good enough, and its exotic features like callbacks are sometimes very useful. So it’s back to the standard solution for me. Good exercise though.

By moving away from ModelFactory I’m losing the feature of allowing database creation only in model specs. That should be easy enough to reinstate in factory_girl with careful configuration, which I’ll post on as soon as I get to it.

Advertisement

Written by dschweisguth

March 2, 2011 at 10:01

Posted in Programming, Rails, Ruby, Testing

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: