当我使用should_receive
和should_not_receive
为遗留项目编写一些测试时,我遇到了一个非常奇怪的问题。
它正在使用rspec-rails 2.10.x
,可以升级到最新rspec-rails 2.x
。但由于代码很复杂,我们无法轻松地将其复制到一个小型的干净项目中。
问题是,我们在测试中有这样的代码:
let(:user) { mock(...) }
it "should do something" do
user.should_receive(:name=)
put :update, :user_id=>1, { :name => "newname" }
end
it "should not do something" do
user.should_not_receive(:name=)
put :update, :user_id=>1, { :name => "newname" }
end
这两个测试都已通过,但唯一的区别是使用should_receive
而另一个使用should_not_receive
。
我做了一些研究,发现有些人有类似的问题:
现在我正在寻找一些解决方法,仍然可以检查是否已调用方法。有什么想法吗?
答案 0 :(得分:0)
问题是在控制器测试中,控制器本身有一个不同的user
实例。
将行puts user.object_id
添加到规范,将puts @user.object_id
添加到控制器,您将看到这一点。 (不同的object_id' s表示它们不是同一个对象。)
即使它们引用同一个实体(即它们都是使用数据库中的相同记录加载并具有相同的id),它们也是不同的实例。更常见的方法是:
user == User.find(user.id) # => true
user.object_id == User.find(user.id).object_id # => false
should_receive
失败了,因为它测试您在测试中的实例上调用:name=
,而您不会。但实际上,您可能正在控制器中的实例上调用:name=
。
should_not_receive
正在传递,因为它测试您从未在规范中的实例上调用:name=
。但这不是你想要测试的。您想测试您从不在控制器中的实例上调用:name=
。
这是一般负面断言的风险。在那些测试中,误报很常见。
更好的测试方法是验证方法的副作用。例如:
expect{ put :update, :user_id=>1, { :name => "newname" }}.
to change{user.reload.name}.from(...).to(...)
# You'll have to fill in the `...`
现在,通过调用user.name=
或以其他方式设置名称(user[:name] = ...
,user.public_send(:name=, "..."
等),是否这样做并不重要。如果您更改了控制器中的实现,则希望它继续通过测试,因为您真正关心的是数据库中的名称是否已更改。但是你现在编写测试的方式,即使你让它们通过,如果你改变了控制器的实现,它们也会失败。