测试在新实例上调用方法的函数的“正确”方法是什么?
我似乎遇到过文献暗指使用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)下列出了这些方法,这意味着我不应该利用这个编写新代码。测试在新实例上调用方法的函数的“正确”方法是什么?
我觉得我经常编写依赖此功能的新规格。一个很好的例子是创建一个新实例然后调用一个方法的任何方法。 (在Rails这里为MyModel是一个ActiveRecord)我经常写做类似下面的方法:
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
的。例如:
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
不过,我认为这更糟糕的是因为)它的长,慢写,和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
皱眉了吗?如果是这样,为什么?
我没有比这两个测试代码更好的解决方案。在stubbing/mocking解决方案中,我会使用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特性,这意味着我的代码可能会使用一些重构。
这是怎样的一个咆哮的,但这里是我的想法:
所津津乐道的文档,甚至列出了“遗留代码”一节下,这些方法(http://www.relishapp.com/rspec/rspec-mocks/v/3-4/docs/working-with-legacy-code/any-instance),这意味着我不应该写利用此新代码。
我不同意这一点。嘲讽/剔除是有效使用时的宝贵工具,应该与断言风格测试一起使用。其原因是,模拟/存根可实现“外置式”测试方法,您可以最大限度地减少耦合并测试高级功能,而无需调用堆栈中的每个小数据库事务,API调用或辅助方法。
问题的确是你想测试状态还是行为?显然,你的应用程序涉及到两个方面,因此将自己束缚于单一测试范例是没有意义的。通过断言/期望的传统测试对于测试状态是有效的,并且很少涉及状态如何改变。另一方面,嘲笑迫使你考虑对象之间的接口和交互,减少状态自身的突变负担,因为你可以存根和填充返回值等。 但是,当使用*_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
以外的任何代码发生变化,它可能不需要重写。将其与在数据库中删除列的断言几乎可以打破整个测试套件的断言进行对比。
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调用)保留模拟/存根,或者使用已定义但未实现的接口时。
你现在在'create_productively'方法中有没有完全相同的问题? –
如果除了在模型上创建和调用方法之外,主题还有更多的功能,这将会更加明显。但无论如何看到我的更新。 –
我的意思是,你现在如何测试'create_productively'。我看不出这有何帮助。也许我只是想念它。 –