我似乎遇到过文献,暗示使用RSpec的any_instance_of
方法(例如expect_any_instance_of
)是不好的做法。津津有味的文档甚至在"使用遗留代码"下列出了这些方法。部分(http://www.relishapp.com/rspec/rspec-mocks/v/3-4/docs/working-with-legacy-code/any-instance)暗示我不应该利用这个来编写新的代码。
我觉得我经常编写依赖此功能的新规范。一个很好的例子是创建新实例然后在其上调用方法的任何方法。 (在MyModel是ActiveRecord的Rails中)我经常编写类似以下内容的方法:
def my_method
my_active_record_model = MyModel.create(my_param: my_val)
my_active_record_model.do_something_productive
end
我通常会编写我的规范,寻找使用do_something_productive
调用的expect_any_instance_of
。 e.g:
expect_any_instance_of(MyModel).to receive(:do_something_productive)
subject.my_method
我能看到的唯一另一种方法是使用这样的一堆存根:
my_double = double('my_model')
expect(MyModel).to receive(:create).and_return(my_double)
expect(my_double).to receive(:do_something_productive)
subject.my_method
然而,我认为这更糟糕,因为a)它写得越来越慢,而b)它比第一种方式更脆弱和白盒子。为了说明第二点,如果我将my_method
更改为以下内容:
def my_method
my_active_record_model = MyModel.new(my_param: my_val)
my_active_record_model.save
my_active_record_model.do_something_productive
end
然后规范的双版本中断,但any_instance_of
版本工作正常。
所以我的问题是,其他开发者如何做到这一点?我使用any_instance_of
的方法是否受到蔑视?如果是这样,为什么?
答案 0 :(得分:2)
我没有比你给出的两个中的任何一个更好的解决方案来测试这样的代码。在存根/模拟解决方案中,我使用allow
而不是expect
进行create
调用,因为create
调用不是规范的要点,但那是一个侧面问题。我同意顽固和嘲弄是痛苦的,但这通常是我所做的。
然而,该代码只有一点功能羡慕。将方法提取到MyModel
可以清除气味并消除测试问题:
class MyModel < ActiveRecord::Base
def self.create_productively(attrs)
create(attrs).do_something_productive
end
end
def my_method
MyModel.create_productively(attrs)
end
# in the spec
expect(MyModel).to receive(:create_productively)
subject.my_method
create_productively
是一种模型方法,因此它可以而且应该使用真实实例进行测试,并且不需要存根或模拟。
我经常注意到需要使用较少常用的RSpec功能意味着我的代码可能会使用一点重构。
答案 1 :(得分:2)
这是一种咆哮,但这是我的想法:
津津有味的文档甚至在“使用遗留代码”部分(http://www.relishapp.com/rspec/rspec-mocks/v/3-4/docs/working-with-legacy-code/any-instance)下列出了这些方法,这意味着我不应该编写利用此代码的新代码。
我不同意这一点。当有效使用时,模拟/存根是一种有价值的工具,应该与断言风格测试一起使用。这样做的原因是模拟/存根启用了一种“从外到内”的测试方法,您可以在这种方法中最小化耦合并测试高级功能,而无需在堆栈中调用每个小的db事务,API调用或辅助方法。
问题确实是你想测试状态或行为吗?显然,你的应用程序涉及两者,所以将自己束缚到单一的测试范例是没有意义的。通过断言/期望的传统测试对于测试状态是有效的,并且很少涉及 状态如何改变。另一方面,mocking强迫你考虑对象之间的接口和交互,减少状态变化的负担,因为你可以存根和shim返回值等等。
但是,我会在使用*_any_instance_of
时提出警告,并尽可能避免使用*_any_instance_of
。这是一种非常生硬的工具,易于滥用,特别是当项目规模较小时,只有在项目规模较大时才成为负债。我经常把my_double = double('my_model')
expect(MyModel).to receive(:create).and_return(my_double)
expect(my_double).to receive(:do_something_productive)
subject.my_method
作为一种气味,可以改进我的代码或测试,但有时候需要使用它。
话虽如此,在你提出的两种方法之间,我更喜欢这个:
my_method
它是明确的,良好隔离的,并且不会因数据库调用而产生开销。如果my_method
的实现发生变化,则可能需要重写,但这没关系。由于它是完全隔离的,如果 var tasks = new Task[2]
{
Task.Factory.StartNew(() => {
//Process Data
}),
Task.Factory.StartNew(() => {
//Sleep while not sync.
})
};
Task.WaitAll(tasks);
之外的任何代码发生变化,则可能不需要重写。将此与断言进行对比,其中在数据库中删除列几乎可以破坏整个测试套件。
答案 2 :(得分:1)
def self.my_method(attrs)
create(attrs).tap {|m| m.do_something_productive}
end
# Spec
let(:attrs) { # valid hash }
describe "when calling my_method with valid attributes" do
it "does something productive" do
expect(MyModel.my_method(attrs)).to have_done_something_productive
end
end
当然,您将对#do_something_productive
本身进行其他测试。
权衡总是一样的:模拟和存根很快,但很脆弱。真实物体较慢但较不易碎,通常需要较少的测试维护。
我倾向于为外部依赖(例如API调用)或使用已定义但未实现的接口保留模拟/存根。