Categories
Innovation

RSpec: verifying model instance creation

UPDATE: I think this post may be a complete waste of time. Just stub out the valid? method on your model to return true or false depending upon what you’re testing. See Ryan Bates’ RailsCast on how he tests controllers. I’m a freaking idiot sometimes.

As a good little rspeccer, I try hard to write my specs to verify behaviour rather than any particular implementation of that behaviour, and, for the moment at least, I’m in the “isolate your controllers from the models” camp. If you’re not in that camp (i.e., you don’t mock and prefer to do functional testing alone), this post probably won’t interest you. One case I often had problems with was model instance creation. There are just so many darn ways to create a new model instance! For a few examples:

@order_item = OrderItem.new(:hi => "hem", :ho => "hum")
@order_item.save!

@order_item = order.order_items.create(:hi => "hem", :ho => "hum")

@order_item = OrderItem.some_custom_creation_method(:hi => "hem", :ho => "hum")

The Problem

When you’re writing your spec (up front, of course!), you don’t want to presume too much about how the implementation will unfold. So, do you stub the create method on the model class? But what if we implement using the new/save combo (as above)? Or, what if we create the model instance through an association?

My Solution

My first pass solution to this problem is the following, based on Matthew Heidemann’s association stubbing technique:

module Spec
  module Mocks
    module Methods
      def stub_creators!(association_name, klass, stubs = {}, valid = true)
        target_mock = Spec::Mocks::Mock.new(klass, {:save => valid, :valid? => valid}.merge!(stubs))
        target_mock.stub!(:save!).and_return do
          target_mock.save
          valid || raise(ActiveRecord::RecordNotSaved)
        end
        klass.stub!(:new).and_return(target_mock)
        klass.stub!(:create).and_return do
          target_mock.save
          target_mock
        end
        klass.stub!(:create!).and_return do
          target_mock.save!
          target_mock
        end
        mock_association = Spec::Mocks::Mock.new(association_name.to_s)
        mock_association.stub!(:create).and_return do
          target_mock.save
          target_mock
        end
        mock_association.stub!(:create!).and_return do
          target_mock.save!
          target_mock
        end
        mock_association.stub!(:build).and_return(target_mock)
        self.stub!(association_name).and_return(mock_association)
        target_mock
      end
    end
  end
end

What this is doing

The thinking here is that, when I write my spec, I don’t want to be concerned with whether the implementation takes the create, new/save, build/save, or other route. In my spec I just want to know that at some point the controller asked for a model instance to be created and saved. The above code, which I put in spec_helper.rb, allows my specs to do just that. Essentially, I stub save so that it returns true or false, depending upon the optional valid parameter, and the other creation stubs derive from that: save!, create, create! on the target association class and the association itself. I also stub out new and build for convenience. This code ensures that save is always called, even though we’ve stubbed out create etc. Now if I need to check that a controller action has caused a model instance to be created, I need only ever check that save is called.

An example

In my specs I call stub_creators! on an instance of the association owner (in our example, the association owner is Order), passing it the name of the association I want to stub (order_items), the model class of the association target (OrderItem), optional stubs for instances of the association target, and whether or not we want the returned model instance to be valid (defaults to true). With this in place, I can do this:

describe OrdersController do
  before(:each)
    @current_user = mock_model(User, :login => "me", :logged_in? => true)
    @order = @current_user.stub_creators!(:order_items, Order)
  end

  it "should create a new order_item" do
    @order.should_receive(:save).and_return(true)
    post 'create'
  end
end

And it doesn’t matter which route the implementation takes to create that model instance. As long as save is called at some point, I know the controller has triggered the creation of the instance somehow.

Thoughts

Now, while this seems to work for me, I don’t really know whether this is kosher. Is it a sensible approach to take? I haven’t tested this extensively; as I said, it’s a first pass. Also, there’s bound to be stuff missing from my solution (for example, it doesn’t handle find_or_create_by_). Can a similar approach be taken to the various ways to delete an object, too? I shall continue to experiment.