我有以下内容:
class Foo
def bar(some_arg)
end
end
它称为Foo.new.bar(some_arg)
。如何在rspec中对此进行测试?我不知道如何知道是否创建了名为Foo
的{{1}}实例。
答案 0 :(得分:4)
receive_message_chain
被认为是一种气味,因为它很容易违反Law of Demeter。
expect_any_instance_of
被认为是一种气味,因为它不特定于正在调用哪个Foo实例。
正如@GavinMiller指出的那样,这些做法通常保留给您无法控制的旧代码。
以下是如何测试Foo.new.bar(arg)
的方法:
class Baz
def do_something
Foo.new.bar('arg')
end
end
describe Baz do
subject(:baz) { described_class.new }
describe '#do_something' do
let(:foo) { instance_double(Foo, bar: true) }
before do
allow(Foo).to receive(:new).and_return(foo)
baz.do_something
end
it 'instantiates a Foo' do
expect(Foo).to have_received(:new).with(no_args)
end
it 'delegates to bar' do
expect(foo).to have_received(:bar).with('arg')
end
end
end
注意:为简单起见,我在这里对arg进行硬编码。但是,您也可以轻松模拟它。在这里显示取决于arg的实例化方式。
编辑
重要的是要注意,这些测试对底层实现非常熟悉。因此,如果您更改实现,则测试将失败。如何解决该问题取决于Baz#do_something
方法的确切作用。
比方说,Baz#do_something
实际上只是基于Foo#bar
从arg
中查找一个值并返回它,而没有在任何地方更改状态。 (这称为Query方法。)在这种情况下,我们的测试根本不需要关心Foo,而应该只关心Baz#do_something
返回的正确值。
另一方面,假设Baz#do_something
实际上确实在某处更改了状态,但没有返回可测试的值。 (这称为Command方法。)在这种情况下,我们需要断言使用正确的参数调用了正确的协作者。但是,我们可以放心,对其他对象的单元测试实际上将测试其内部,因此我们可以使用模拟作为占位符。 (我上面显示的测试是各种各样的。)
桑迪·梅斯(Sandi Metz)早在2013年就对此进行了精彩的评论talk。她提到的技术细节已经改变。但是,今天如何测试100%相关性的核心内容。
答案 1 :(得分:0)
如果要在另一个类规范(例如BazClass)中模拟此方法,则模拟方法将只返回一个包含您期望的信息的对象。例如,如果在此Baz#some_method规范中使用Foo#bar,则可以执行以下操作:
# Baz#some_method
def some_method(some_arg)
Foo.new.bar(some_arg)
end
#spec for Baz
it "baz#some_method" do
allow(Foo).to receive_message_chain(:bar).and_return(some_object)
expect(Baz.new.some_method(args)).to eq(something)
end
否则,如果您希望Foo实际调用该方法并运行它,则只需定期调用该方法
#spec for Baz
it "baz#some_method" do
result = Baz.new.some_method(args)
@foo = Foo.new.bar(args)
expect(result).to eq(@foo)
end
编辑:
it "Foo to receive :bar" do
expect(Foo.new).to receive(:bar)
Baz.new.some_method(args)
end
答案 2 :(得分:0)
最简单的方法是使用expect_any_instance_of
。
expect_any_instance_of(Foo).to receive(:bar).with(expect_arg).and_return(expected_result)
也就是说,不建议使用此方法,因为它很复杂,有设计气味,并且可能导致奇怪的行为。建议的用法用于您无法完全控制的旧代码。
推测您的代码是什么样的,我希望这样:
class Baz
def do_stuff
Foo.new.bar(arg)
end
end
it 'tests Baz but have to use expect_any_instance_of' do
expect_any_instance_of(Foo).to receive(:bar).with(expect_arg).and_return(expected_result)
Baz.do_stuff
# ...
end
如果您遇到这种情况,最好将类实例化为默认参数,如下所示:
class Baz
def do_stuff(foo_instance = Foo.new)
foo_instance.bar(arg)
end
end
这样,您可以传递模拟来代替默认实例:
it 'tests Baz properly now' do
mock_foo = stub(Foo)
Baz.do_stuff(mock_foo)
# ...
end
这称为依赖注入。有点forgotten art in Ruby,但是如果您阅读有关Java测试模式的内容,则会发现它。尽管一旦您开始走这条路,兔子洞就会变得很深,对Ruby来说可能会显得过分杀伤。