Reputation: 427
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:
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
Upvotes: 0
Views: 321
Reputation: 427
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
.
Upvotes: 0