2

I have this problem related to testing model errors with Mocha:

  1. This is my controller (Api / Artist controller):

    class Api::ArtistsController < ApplicationController
       respond_to :json
    
       def create
         artist = Artist.new(params[:artist])
         if artist.save <-- This is where the test fails
           render :json=>artist
         else
           respond_with artist
         end
       end
    end
    
  2. This is my model (Artist Model):

    class Artist < ActiveRecord::Base
       include Core::BaseModel
       attr_accessible :name
       has_many :albums
       validates :name, :presence=>true, :uniqueness=>{:case_sensitive=> false}
       default_scope where :deleted=>false
    end
    
  3. This is the test where it fails, about Artist controller:

    it "should not save a duplicated artist" do
      Artist.any_instance.stubs(:is_valid?).returns(false)
      Artist.any_instance.stubs(:errors).returns({:name=>[I18n.t('activerecord.errors.messages.taken')]})
      post :create, :format => :json
    
      expect(response).not_to be_success
      expect(response.code).to eq("422")
    
      results = JSON.parse(response.body)
      expect(results).to include({
          "errors"=>{
            "name"=>[I18n.t('activerecord.errors.messages.taken')]
          }
        })
    end
    

When I run the tests, this is the error I get on the above test:

Failure/Error: post :create, :format => :json
     NoMethodError:
       undefined method `add_on_blank' for {}:Hash
     # ./app/controllers/api/artists_controller.rb:17:in `create'
     # ./spec/controllers/api/artists_controller_spec.rb:56:in `block (3 levels) in <top (required)>'

I'm starting to use Mocha, so I don't know if there's a way to test the json result for the specific case when I want to test the validation for the duplicated name.

diegocst90
  • 61
  • 7

1 Answers1

2

ActiveRecord::Base#errors (i.e. Artist#errors) isn't a simple hash. It's supposed to be an instance of ActiveModel::Errors. You're stubbing it with a hash, and ActiveRecord is trying to call add_on_blank on it, which is failing.

I don't think save invokes is_valid? at all, and I suspect it's running the validations and then trying to call add_on_blank to append an error, but since you've stubbed out errors, that's failing.

This isn't really a good way to test the controller. It's making too many assumptions about the internals of Artist. You're also testing things that aren't part of the controller at all; errors isn't referenced anywhere in the action. The only behavior worth testing in the controller is whether or not it creates an Artist; if that Artist fails to save, that it renders JSON with it; and if the save succeeds, that it redirects. That's all of the controller's responsibility.

If you want to test that errors are rendered a certain way, you should write a separate view spec. If you want to test that missing fields generate errors, you should write a model spec. If you don't want to write a view spec, it's still sufficient to rely on the model to populate errors (tested in a model spec), and in your controller, just test that render is called with json set to the Artist instance.

Generally speaking it's best to avoid stubbing as much as possible, but in this case, the only things I'd consider stubbing are Artist.new to return a mock, and save on that mock to return false. Then I'd check to make sure it rendered with the mock.

The easier option is to just create an actual Artist record, then call post with duplicate params to trigger a validation failure. The downside is that you hit the database, and avoiding that in a controller spec is laudable, but generally more convenient. You could instead do that in a Capybara feature spec if you want to avoid DB hits in your controller specs.

If you want to try testing the way you are, you can manually create an instance of ActiveModel::Errors and populate that, or stub methods on it, and stub out Artist.any_instance.stubs(:errors) to return your mock with ActiveModel::Errors-compatible behavior, but that's a lot of mocking.

One final tip: don't use post :create, :format => :json. Use xhr :post, :create to generate a real Ajax request rather than relying on a format param. It's a more robust test of your routing and response code.

Jim Stewart
  • 16,964
  • 5
  • 69
  • 89