Search code examples
rspecruby-on-rails-3.1factory-bot

RSpec tests failing when dev and prod work - RSpec quirk, virtual attribute, or form issue?


I'm writing a basic application using RoR, and I'm testing with RSPec (and Factory Girl). My app is working on both Dev and Prod, but I can't get all of my RSpec tests to pass. I suspect this is some sort of RSpec quirk or that it's related to mass assignment (of virtual attributes). I'll give as much information as possible, but please let me know if I leave out anything pertinent. (I've been trolling for a while, but this is my first post, so I'm sure I'll forget something.)

EDIT: I should say that all tests were passing before I implemented the virtual attribute (which I did to use the JQuery-ui autocomplete on this username field).

Here's my setup:

Rails 3.1.1
Ruby 1.9.2p290
RSpec 2.6.4 (that's what I get doing rspec -v from the command line, but the Gemfile says 2.6.1 - I'm not sure what's up with that)
Factory Girl 1.0 (factory_girl_rails gem) Dev site is SQLite3
Prod is hosted on heroku (postgres)

First, here is a failing result from RSpec (on item_shares_controller.rb) - I'm only including one failure to save space. The issue is the same on each failure (of 10). I'm focused on this one because it I'm able to create a new item_share on dev/prod, so this should definitely pass the test:

  5) ItemSharesController POST 'create' success should create an item_share
     Failure/Error: post :create, @attr
     ActiveRecord::RecordInvalid:
       Validation failed: Receiver is required. Please enter a valid username.
     # ./app/controllers/item_shares_controller.rb:25:in `create'
     # ./spec/controllers/item_shares_controller_spec.rb:71:in `block (5 levels) in <top (required)>'
     # ./spec/controllers/item_shares_controller_spec.rb:70:in `block (4 levels) in <top (required)>'

NOTE: I've marked the relevant lines in the code below (25, 70, 71).

So the issue seems to be that the controller isn't getting the :receiver_username attribute from my form. Here's the form:

<%= form_for(@item_share) do |f| %>
  <%= render 'shared/error_messages', :object => f.object %>
  <div class="field">
    <%= fields_for :item do |i| %>
      <%= i.label "What?" %></br>
      <%= i.text_field :link_url, { :placeholder => 'http://...' }  %>
    <% end %>
  </div>
  <div class="field">
      <%= f.label "With who?" %></br>
      <%= f.text_field :receiver_username, :placeholder => 'JohnDoe123', data: { autocomplete_source: autocomplete_users_path } %>
  </div>
  <div class = "field">
    <%= f.label "Why?" %></br>
    <%= f.text_area :note, { :placeholder => '(Optional) In 140 characters or fewer, add a note.' }  %>
  </div>
  <div class="actions">
    <%= f.submit "Submit" %>
  </div>
<% end %>

Note that the relevant field here is the :receiver_username field, which will pass the username to the controller. This field is a virtual attribute of the item_share model (item_share.rb is below) and you can see I'm using jQuery-ui's autocomplete function on that field.

Here's the controller :create method (I'm not putting the whole controller here for brevity):

#item_shares_controller.rb

def create

  @item = Item.new()
  @item.link_url = params[:item] ? params[:item][:link_url] : ""


  params_username = params[:item_share] ? params[:item_share][:receiver_username] : ""

  @receiver = User.find(:first, :conditions => [ "lower(username) = ?", params_username.to_s.downcase ] )

  @giver = current_user

  @note = params[:item_share] ? params[:item_share][:note] : ""

  @item_share = current_user.item_shares_given.build(:item => @item, :receiver => @receiver, :note => @note)

  # The next line is 25 as called out in the failure
  if  @item_share.save!()
    if @receiver.send_email? && !(@receiver == @giver)
      UserMailer.new_share_notification(@receiver, @giver, @item_share).deliver
    end
    flash[:success] = "You shared a link!"
    redirect_to root_path
  else
    @list_items = []
    flash.now[:error] = "The link was not shared."
    render 'pages/home'
  end

end

Here is the item_share model (where I've defined receiver_username as a virtual attribute and you can see the setter/getter methods at the bottom of the model):

# item_share.rb

class ItemShare < ActiveRecord::Base
  attr_accessible :note, :item, :receiver, :share_source_user_id, :share_target_user_id, :item_id

  # ItemShares relation to Users
  belongs_to :giver, :class_name => "User",
    :foreign_key => "share_source_user_id"
  belongs_to :receiver, :class_name => "User",
    :foreign_key => "share_target_user_id"

  # ItemShares relation to Items
  belongs_to :item

  default_scope :order => 'item_shares.created_at DESC'

  validates :note, :length => { :within => 0..140, :message => "must be 140 characters or fewer." } 
  validates_associated :item, :message => "is not valid. Please enter a valid URL."
  validates :receiver, :presence => {:message => "is required. Please enter a valid username." }
  validates :share_source_user_id, :presence => true

  def receiver_username
    receiver.try(:username)
  end

  def receiver_username=(username)
    self.receiver = Receiver.find_by_username(username) if username.present?
  end

end

And here's some of my item_shares_controller_spec.rb file (I've tried to trim it to show what's relevant, but I'm not including the whole thing, again for brevity):

# item_shares_controller_spec.rb

require 'spec_helper'

describe ItemSharesController do
  render_views

  describe "POST 'create'" do

    before(:each) do
      @user = test_sign_in(Factory(:user))
    end

    describe "success" do

      before(:each) do
        @receiver = Factory(:user, :name => Factory.next(:name),
                            :username => Factory.next(:username),
                            :email => Factory.next(:email))
        @item = Factory(:item)
        @attr = {:item =>{:link_url=>@item.link_url}, :item_share =>{:receiver_username=>@receiver.username}, :item_share=>{:note=>"Note"}}
      end

      it "should create an item_share" do
        # Next lines are 70 and 71 as called out in the failure
        lambda do
          post :create, @attr
        end.should change(ItemShare, :count).by(1)
      end
    end
  end
end

And here's my factories.rb file (I don't think this is related to the issue, but I might as well include it):

#factories.rb

# By using the symbol ':user', we get Factory Girl to simulate the User model.
Factory.define :user do |user|
  user.name                  "Michael Hartl"
  user.username              "MichaelHartl"
  user.email                 "[email protected]"
  user.password              "foobar"
  user.password_confirmation "foobar"
  user.send_email            true
end

Factory.sequence :name do |n|
  result = "PersonAAA"
  n.times { result.succ! }
  result
end

Factory.sequence :email do |n|
  "person-#{n}@example.com"
end

Factory.sequence :username do |n|
  "Person#{n}"
end

Factory.define :item do |item|
  item.link_url              "http://www.google.com"
end

Factory.define :item_share do |item_share|
  item_share.note                  "Foo bar"
  item_share.association           :giver, :factory => :user
  item_share.association           :receiver, :factory => :user
  item_share.association           :item, :factory => :item
end

Ok, so back to the failure. It looks like RSpec isn't able to pass the username (which I would expect to be in params[:item_share][:receiver_username]) to the controller (when testing) for some reason. I've tried several things and am still stumped. Here's what I've tried:

  • I'm using a params_username.to_s.downcase in the controller before I search for the User. I added that and it helped (RSpec was failing more tests before I added that), but didn't resolve this issue.
  • I've tried adding :receiver_username to the attr_accessible line in the item_share.rb model.
  • I've tried messing with the validations in the item_share.rb model (e.g., changing the validates :receiver line to a validates_associated :receiver and some other variations on that sort of thing).
  • rake db:test:prepare - didn't help

As I said, everything is working on both Dev and Prod. The only indication that something is "wrong" is with the RSpec failures.

Any ideas how I can get these tests to pass?

Thanks

Josh


Solution

  • I added this as a comment to my original question, but that obviously doesn't mark the question itself as "answered". For posterity, here's a copy/paste of my answer:

    I think I found the issue - I wasn't defining the parameters correctly in my @attr variable. I had

    # item_shares_controller_spec.rb
    describe ItemSharesController do
      render_views
    
      describe "POST 'create'" do
    
        before(:each) do
          @user = test_sign_in(Factory(:user))
        end
    
        describe "success" do
    
          before(:each) do
            @attr = {:item =>{:link_url=>@item.link_url}, :item_share =>{:receiver_username=>@receiver.username}, :item_share=>{:note=>"Note"}} 
    

    ...when I should have had

    @attr = {:item =>{:link_url=>@link_url}, :item_share=>{:receiver_username=>@receiver.username, :note=>"Note"}} 
    

    Note that I was defining :item_share twice within the @attr definition. I think RSpec was ignoring the first :item_share parameter and taking only the second one, :note.