josemota
josemota

Reputation: 984

Stubbing model attributes in controller tests

I am finding it very hard to stub certain attributes of a model on a controller test. I want to make sure to stub as little as possible.


EDIT: I have been demoved of using stubs for such integration. I understood that the stubs won't reach the action call. The correct question would now be:

How can one use mocks and stubs to simulate a certain state in a Rails controller test?


So I've reached something like the following:

Spec

require 'spec_helper'

describe TeamsController do
  let(:team) { FactoryGirl.create :team }

  context "having questions" do
    let(:competition) { FactoryGirl.create :competition }

    it "allows a team to enter a competition" do
      post(:enter_competition, id: team.id, competition_id: competition.id)

      assigns(:enroll).team.should == team
      assigns(:enroll).competition.should == competition
    end
  end

  # ...
end

Factories

FactoryGirl.define do
  factory :team do
    name "Ruby team"
  end

  factory :competition, class: Competition do
    name "Competition with questions"

    after_create do |competition|
      competition.
        stub(:questions).
        and_return([ 
          "something"
        ])
    end
  end

  factory :empty_competition, class: Competition do
    name "Competition without questions"
    questions []

    after_create do |competition|
      competition.stub(:questions).and_return []
    end
  end
end

Production code

class TeamsController < ApplicationController
  def enter_competition
    @team = Team.find params[:id]
    @competition = Competition.find params[:competition_id]
    @enroll = @team.enter_competition @competition

    render :nothing => true
  end
end

class Team < ActiveRecord::Base
  def enter_competition competition
    raise Competition::Closed if competition.questions.empty?

    enroll = Enroll.new team: self, competition: competition
    enroll.save
    enroll
  end
end

When I run the test, the questions attribute comes as being nil and so the test fails in the model when checking for nil.empty?.

Why isn't the stub being used so that the state of that message is correctly used? I expected that @competition.questions would be [ "question" ] but instead I get nil.

Upvotes: 0

Views: 2201

Answers (2)

Joe Ferris
Joe Ferris

Reputation: 2732

The problem you're running into is that stub works on an instance of a Ruby object; it doesn't affect all ActiveRecord objects that represent the same row.

The quickest way to fix your test would be to add this to your test, before the post:

Competition.stub(:find).and_return(competition)

The reason that's necessary is that Competition.find will return a fresh Competition object that doesn't have questions stubbed out, even though it represents the same database row. Stubbing find as well means that it will return the same instance of Competition, which means the controller will see the stubbed questions.

I'd advise against having that stub in your factory, though, because it won't be obvious what's stubbed as a developer using the factory, and because it means you'll never be able to test the real questions method, which you'll want to do in the Competition unit test as well as any integration tests.

Long story short: if you stub out a method on an instance of your model, you also need to stub out find for that model (or whatever class method you're using to find it), but it's not a good idea to have such stubs in a factory definition.

Upvotes: 3

Art Shayderov
Art Shayderov

Reputation: 5110

When you call create on FactoryGirl, it creates database records which you then retrieve back in your controller code. So the instances you get (@team, @competition) are pure ActiveRecord, without any methods stubbed out.

Personally I would write you test like this (not touching database at all):

let(:team) { mock_model(Team) }
let(:competition) { mock_model(Competition) }

before do
  Team.stub(:find) { team }
  Competition.stub(:find) { competition }
end

and then in your test something like this:

it "should call enter_competition on @team with @competition" do
  team.should_receive(:enter_competition).with(competition)

  post :enter_competition, id: 7, competition_id: 10

I don't really understand what your controller is supposed to do or what are you testing for that matter, sorry :(

Upvotes: 1

Related Questions