Wednesday, February 1, 2012

Fixing Rails 3 Automatically Generated Controller Tests to Support Unique Attribute Validation Using FactoryGirl

We used the Rails 3 scaffold generator to generate a lot of tests- for controllers, views, etc. We then added ActiveRecord model attribute validation and validated some attributes to be unique. We noticed sometime after that these tests were failing.

Although we didn't have to use FactoryGirl to refactor common data required for models instances for tests, it reduced the amount of code we had to write.

We had a problem with both the rubygems repo's version of FactoryGirl 1.5.0 and 1.6.0 having some wierd UTF issues with the gemspec, so we got it from GitHub instead. Here's an example from Gemfile:

group :development, :test do
  gem 'rspec-rails'
  gem 'factory_girl_rails', '1.6.0', :git => "git://github.com/thoughtbot/factory_girl_rails.git"
end

We used bundle to install and then added FactoryGirl to spec/spec_helper.rb under the other requires:

require 'factory_girl'

We started by trying to use FactoryGirl's ability to load up each model's factory as its own file because that seemed cleaner, but because of some factories being dependent on other factories, we ended up defining all factories in spec/factories.rb. That way, the load order would be correct.

After we created the factories, when we blew away our sqlite database with rake db:drop, rake db:migrate failed when attempting to run the migrations to create those tables because FactoryGirl choked not being able to find the tables associated with the models we referenced.

Even though there are other ways to get around this, to make it easier, we check for a table that indicates that the schema is setup. This is not optimal, but it works for now. Here's an example:

if ActiveRecord::Base.connection.table_exists? 'locations'
  
  Factory.define :user do |u|
    u.sequence(:netid) {|n| "test#{n}" }
  end

  Factory.define :location do |l|
    l.sequence(:name) {|n| "test#{n}" }
  end
  
  Factory.define :widget do |b|
    b.location (Factory.build(:location))
    b.sequence(:number) {|n| "test#{n}" }
  end
end

After running rspec at this point, we still saw all of the failing tests.

Then we had to edit the tests. Here is a sample test so you can see the replacements we did. Basically we replaced the valid_attributes method with:

before(:each) do
  @widget = Factory(:widget)
end

Then we replaced a lot of references to widget with @widget, with some exceptions. We had to change the way that we sent widget instances into posts, for example:

post :create, :widget => Factory.build(:widget).attributes.symbolize_keys

And did similar for :update. Here is an example rspec controller test with changes made:

require 'spec_helper'

describe WidgetsController do

  before(:each) do
    @widget = Factory(:widget)
  end

  describe "GET index" do
    it "assigns all widgets as @widgets" do
      get :index
      assigns(:widgets).should eq([@widget])
    end
  end

  describe "GET show" do
    it "assigns the requested widget as @widget" do
      get :show, :id => @widget.id
      assigns(:widget).should eq(@widget)
    end
  end

  describe "GET new" do
    it "assigns a new widget as @widget" do
      get :new
      assigns(:widget).should be_a_new(Widget)
    end
  end

  describe "GET edit" do
    it "assigns the requested widget as @widget" do
      get :edit, :id => @widget.id
      assigns(:widget).should eq(@widget)
    end
  end

  describe "POST create" do
    describe "with valid params" do
      it "creates a new Widget" do
        expect {
          post :create, :widget => Factory.build(:widget).attributes.symbolize_keys
          
        }.to change(Widget, :count).by(1)
      end

      it "assigns a newly created widget as @widget" do
        post :create, :widget => Factory.build(:widget).attributes.symbolize_keys
        assigns(:widget).should be_a(Widget)
        assigns(:widget).should be_persisted
      end

      it "redirects to the created widget" do
        post :create, :widget => Factory.build(:widget).attributes.symbolize_keys
        response.should redirect_to(Widget.last)
      end
    end

    describe "with invalid params" do
      it "assigns a newly created but unsaved widget as @widget" do
        Widget.any_instance.stub(:save).and_return(false)
        post :create, :widget => {}
        assigns(:widget).should be_a_new(Widget)
      end

      it "re-renders the 'new' template" do
        Widget.any_instance.stub(:save).and_return(false)
        post :create, :widget => {}
        response.should render_template("new")
      end
    end
  end

  describe "PUT update" do
    describe "with valid params" do
      it "updates the requested widget" do
        Widget.any_instance.should_receive(:update_attributes).with({'these' => 'params'})
        put :update, :id => @widget.id, :widget => {'these' => 'params'}
      end

      it "assigns the requested widget as @widget" do
        put :update, :id => @widget.id, :widget => @widget.attributes.symbolize_keys
        assigns(:widget).should eq(@widget)
      end

      it "redirects to the widget" do
        put :update, :id => @widget.id, :widget => @widget.attributes.symbolize_keys
        response.should redirect_to(@widget)
      end
    end

    describe "with invalid params" do
      it "assigns the widget as @widget" do
        Widget.any_instance.stub(:save).and_return(false)
        put :update, :id => @widget.id, :widget => {}
        assigns(:widget).should eq(@widget)
      end

      it "re-renders the 'edit' template" do
        Widget.any_instance.stub(:save).and_return(false)
        put :update, :id => @widget.id, :widget => {}
        response.should render_template("edit")
      end
    end
  end

  describe "DELETE destroy" do
    it "destroys the requested widget" do
      expect {
        delete :destroy, :id => @widget.id
      }.to change(Widget, :count).by(-1)
    end

    it "redirects to the widgets list" do
      delete :destroy, :id => @widget.id
      response.should redirect_to(widgets_url)
    end
  end

end

No comments: