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.