0

I'm trying to test a controller to ensure that only an authorized party can view the correct child object using RSpec. I cant figure out what I'm doing wrong as I'm getting this error:

ActiveRecord::RecordInvalid: Validation failed: Company can't be blank

I have a Plan object and a Company object. The Store can have many plans (think of a pest control Company). I want to test that given a known scenario I can retrieve the plan fo the Company (assuming there is only one).

The Plan looks like this:

class Plan < ActiveRecord::Base
  before_save :default_values

  # Validation
  validates :amount, :presence => true
  validates :company, :presence => true

  # Plans belong to a particular company.
  belongs_to :company, :autosave => true    

  scope :find_all_plans_for_company, lambda {
    |company| where(:company_id => company.id)
  }
  # Other code ...

end 

The Company looks like this:

class Company < ActiveRecord::Base
  validates :name, :presence => true
  validates :phone1, :presence => true

  validates_format_of :phone1, :phone2,
                      :with => /^[\(\)0-9\- \+\.]{10,20}$/,
                      :message => "Invalid phone number, must be 10 digits. e.g. - 415-555-1212",
                      :allow_blank => true,
                      :allow_nil => true

  has_many :users
  has_many :plans

end

.. controller looks like this

def index
    @plans = Plan.find_all_plans_for_company(current_user.company)

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @plans }
    end
  end

.. and my RSpec test looks like this (excuse me if its full of gimmickery, I'm just splunking around with it and cannot get it to work).

describe PlansController do

  def valid_attributes
    {
        :company_id => 1,
        :amount => 1000
    }
  end

  describe "GET index" do
    it "should return the Plans for which this users company has" do

      @company = mock_model(Company, :id => 1, :name => "Test Company", :phone1 => "555-121-1212")
      Company.stub(:find).with(@company.id).and_return(@company)

      controller.stub_chain(:current_user, :company).and_return(@company)

      plan = Plan.create! valid_attributes

      get :index, {}
      assigns(:plans).should eq([plan])
    end

    # Other tests ...
  end

end

The problem is, when I try this (or any of the crazy other variants I've tried) I get this error:

ActiveRecord::RecordInvalid: Validation failed: Company can't be blank

I'm not sure why this is happening as I thought the Company.stub call would handle this for me. But apparently not.

What am I missing here and what am I doing wrong? How can I get this test to pass?

7
  • Can you post the controller code as well? Commented Aug 3, 2012 at 23:29
  • @shioyama - The controller has been added. Commented Aug 4, 2012 at 1:27
  • Sorry I realize now the problem happens before get :index, {}, so the controller code doesn't really matter. Commented Aug 4, 2012 at 2:24
  • I think @Erez Rabih's suggestion #1 below is the likely cause. Do you have any attr_accessible statements in your Plan model? If not add the one below and see if it changes anything. Commented Aug 4, 2012 at 2:29
  • @shioyama - Thank you your suggestions, unfortunately even after adding attr_accessible it still fails. It seems that the #2 below seems to have fixed the problem, but I thought mocks would solve this. Commented Aug 4, 2012 at 3:27

3 Answers 3

2

Let's peel back the layers on this spec, to make sure things make sense (and to make sure I understand what's going on). First, what are you testing?

it "should return the Plans for which this users company has" do

  ...

  assigns(:plans).should eq([plan])

So you want to check that the plans associated with the company of the current user are assigned to @plans. We can stub or mock out everything else.

Looking at the controller code, we have:

def index
  @plans = Plan.find_all_plans_for_company(current_user.company)

What do we need to get this to work, without hitting the database and without depending on the models?

First of all, we want to get a mock company out of current_user.company. This is what these two lines in your spec code do:

  @company = mock_model(Company, :id => 1, :name => "Test Company", :phone1 => "555-121-1212")
  controller.stub_chain(:current_user, :company).and_return(@company)

This will cause current_user.company to return the mock model @company. So far so good.

Now to the class method find_all_plans_for_company. This is where I'm a bit confused. In your spec, you stub the find method on Company to return @company for id = 1.

But really, wouldn't it suffice just to do something like this in your controller code?:

  @plans = current_user.company.plans

If you did it this way, then in your test you could just mock a plan, and then return it as the plans association for your mock company:

  @plan = mock_model(Plan)
  @company = mock_model(Company, :plans => [ @plan ])
  controller.stub_chain(:current_user, :company).and_return(@company)

Then the assignment should work, and you don't need to actually create any model or hit the database. You don't even need to give your mock company an id or any other attributes, which anyway are irrelevant to the spec.

Maybe I'm missing something here, if so please let me know.

Sign up to request clarification or add additional context in comments.

Comments

1

Why do you need to mock?

My standard testing setup is to use Database Cleaner which clears out the database from any records created during tests. In this way, the tests are run with real database records which are consequently deleted from the test database after each test.

You might also like taking a look at Factory Girl for creating instances of your models during testing (makes it easy to create 10 company records, for example).

See:

5 Comments

I'm also using FactoryGirl with RSpec. This makes writing the tests much simpler, which is also a factor to consider. I make sure the tests clean up after themselves (this is easy to do with RSpec since all of the rows that were inserted during the test are rolled back after the test anyway)
There is a tradeoff here: using actual records in the database means you're tightly coupling your controller specs to model code, and also will take more time to execute because you're hitting the database. Generally speaking, if the specs are not too complicated, it's better to mock/stub out calls to model classes/instances.
But if you completely decouple your models from your controller tests, then in addition to your model tests and controller tests you will need a third set of tests to ensure they work together. Since the system ultimately works as a whole, surely the only benefit of this approach is the speed of testing?
No, not exactly. Decoupling specs means that when something goes wrong, you get a more precise failure. i.e. if your model is broken your controller specs won't all suddenly start failing if they are decoupled. It's true that you have to add integration tests to patch the space between models, controllers etc. but you need those anyways.
Gotcha. Thanks for taking the time to be a part of this conversation!
0

I have three thoughts coming up that could resolve your issue:

  1. Try adding attr_accessible :company_id to Plan class.

  2. Because mock_model does not actually save to the database when you create a Plan with company_id of 1 it fails validation since it is not present in the database.

  3. Ensure before_save :default_values in Plan class does not mess with company_id attribute of the newly created instance.

2 Comments

#1 and #3 didnt have any effect. But I changed this line: @ company = mock_model(Company, :id => 1, :name => "Test Company", :phone1 => "555-121-1212") to this: @ company = Company.create(:name => "Test Gym", :phone1 => "555-555-5555") and it works. Why can I not use a mock here?
Simple, you're creating a real Plan and giving it a company_id of 1, but you never created a real company in the DB with an id of 1, you just created a mock model in memory. So validation of the presence of company fails. The company doesn't exist.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.