开发和生产工作时 RSpec 测试失败 - RSpec 怪癖、虚拟属性或表单问题?
我正在使用 RoR 编写一个基本应用程序,并且正在使用 RSPec(和 Factory Girl)进行测试。我的应用程序可以在开发和生产环境中运行,但我无法通过所有 RSpec 测试。我怀疑这是某种 RSpec 怪癖,或者它与(虚拟属性的)批量分配有关。我将提供尽可能多的信息,但如果我遗漏了任何相关内容,请告诉我。 (我已经拖钓了一段时间,但这是我的第一篇文章,所以我确信我会忘记一些东西。)
编辑:我应该说在我实现虚拟属性之前所有测试都已通过(我这样做是为了在此用户名字段上使用 JQuery-ui 自动完成功能)。
这是我的设置:
Rails 3.1.1
红宝石 1.9.2p290
RSpec 2.6.4(这就是我从命令行执行 rspec -v 的结果,但 Gemfile 说是 2.6.1 - 我不确定这是怎么回事)
工厂女孩 1.0 (factory_girl_rails gem) 开发站点是 SQLite3
Prod 托管在 heroku (postgres) 上,
首先,这是 RSpec 的失败结果(在 item_shares_controller.rb 上) - 我只包含一个失败以节省空间。每次失败(共 10 次)的问题都是相同的。我专注于这一点,因为我能够在 dev/prod 上创建一个新的 item_share,所以这肯定会通过测试:
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)>'
注意:我已经在下面的代码中标记了相关行(25, 70, 71 )。
所以问题似乎是控制器没有从我的表单中获取 :receiver_username 属性。格式如下:
<%= 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 %>
请注意,此处的相关字段是 :receiver_username 字段,它将把用户名传递给控制器。该字段是 item_share 模型的虚拟属性(item_share.rb 如下),您可以看到我在该字段上使用 jQuery-ui 的自动完成功能。
这是控制器 :create 方法(为了简洁起见,我没有将整个控制器放在这里):
#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
这是 item_share 模型(其中我将receiver_username 定义为虚拟属性,您可以在控制器底部看到 setter/getter 方法) 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
这是我的一些 item_shares_controller_spec.rb 文件(我尝试对其进行修剪以显示相关内容,但我没有包含整个内容,再次用于简洁):
# 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
这是我的factory.rb 文件(我认为这与问题无关,但我不妨将其包括在内):
#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
好的,那么回到失败。由于某种原因,RSpec 似乎无法将用户名(我希望位于 params[:item_share][:receiver_username] 中)传递给控制器(测试时)。我已经尝试了几件事,但仍然感到困惑。这是我尝试过的:
- 我在控制器中使用 params_username.to_s.downcase 之前 搜索用户。我添加了这一点,它有帮助(RSpec 失败了 在我添加之前进行了更多测试),但没有解决这个问题。
- 我尝试将 :receiver_username 添加到 attr_accessible 行中 item_share.rb 模型。
- 我尝试过搞乱验证 item_share.rb 模型(例如,更改 validates :receiver 行 到 validates_linked :receiver 以及其他一些变体 之类的事情)。
- rake db:test:prepare - 没有帮助
正如我所说,一切都在开发和生产中运行。唯一表明出现“错误”的情况是 RSpec 故障。
有什么想法可以让这些测试通过吗?
谢谢乔希
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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
我将此作为对我原来问题的评论添加,但这显然并不将问题本身标记为“已回答”。对于后代,这是我的答案的复制/粘贴:
我想我发现了问题 - 我没有在我的
@attr
变量中正确定义参数。我……当我应该有的时候
请注意,我在
@attr
定义中定义了 :item_share 两次。我认为 RSpec 忽略了第一个:item_share
参数,只采用第二个:note
参数。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...when I should have had
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
.