
[英]How to test in Ruby/Rails if method has been called in class body?
[英]How to properly mock an instance of an Active Record model to test that a method has been called
通过以下 controller 操作:
def create
my_foo = MyFoo.find params[:foo_id]
my_bar = my_foo.my_bars.create! my_bar_params
my_bar.send_notifications
redirect_to my_bar
end
在我的测试中,我试图断言方法send_notifications
) 在my_bar
中被调用,它是 AR model 的一个实例。
测试这一点的一种方法是确保发送通知( expect { request}.to enqueue_mail...
)。 但这可能不是一个好的做法,因为它会穿透抽象层。
另一种选择是使用expect_any_instance_of
:
it 'sends the notifications' do
expect_any_instance_of(MyBar).to receive :send_notifications
post my_bars_path, params: { my_bar: valid_attributes }
end
我喜欢这种方法,因为它简洁明了,但 RSpec 的创建者似乎弃用了它。
我尝试的另一种方法需要 mocking 很多 AR 方法:
it 'sends the notifications' do
my_bar = instance_double MyBar
allow(MyBar).to receive(:new).and_return my_bar
allow(my_bar).to receive :_has_attribute?
allow(my_bar).to receive :_write_attribute
allow(my_bar).to receive :save!
allow(my_bar).to receive :new_record?
expect(my_bar).to receive :send_notifications
allow(my_bar).to receive(:to_model).and_return my_bar
allow(my_bar).to receive(:persisted?).and_return true
allow(my_bar).to receive(:model_name).and_return ActiveModel::Name.new MyBar
post my_bars_path, params: { my_bar: valid_attributes }
end
超出expect
的allow
在那里模拟@my_bar = @my_foo.my_bars.create! my_bar_params
@my_bar = @my_foo.my_bars.create! my_bar_params
。 expect
下的allow
的 rest 在那里模拟redirect_to @my_bar
。
我不知道这是否是 RSpec 的创建者希望我们编写的,但它似乎不太符合人体工程学。
所以,我的问题是:有没有其他方法可以编写这样的测试,它不涉及 mocking 大量 AR 内部构件,也不需要我更改 controller 操作中的代码?
我喜欢 [using expect_any_instance_of] 因为它简洁明了,但 RSpec 的创建者似乎弃用了它。
他们有充分的理由阻止它。 如果 controller 代码发生变化并且其他东西调用了 send_notifications 怎么办? 测试会通过。
必须使用 expect_any_instance_of 或 allow_any_instance_of 表明代码做得太多,可以重新设计。
有什么不能通过添加另一个抽象层来解决的?
def create
my_bar.send_notifications
redirect_to my_bar
end
private
def my_foo
MyFoo.find params[:foo_id]
end
def my_bar
my_foo.my_bars.create! my_bar_params
end
现在您可以模拟my_bar
以返回一个双精度值。 如果该方法更广泛地使用了 my_bar,您还可以返回一个真正的 MyBar。
it 'sends the notifications' do
bar = instance_double(MyBar)
allow(@controller).to receive(:my_bar).and_return(bar)
post my_bars_path, params: { my_bar: valid_attributes }
end
在 controller 中封装查找和创建模型和记录是一种常见模式。
它还在测试和方法之间创建了一种令人愉悦的对称性,表明该方法正在做它必须做的事情,而不是更多。
或者,使用服务 object 来处理通知并进行检查。
class MyNotifier
def self.send(message)
...
end
end
class MyBar
NOTIFICATION_MESSAGE = "A bar, a bar, dancing in the night".freeze
def send_notification
MyNotifier.send(NOTIFICATION_MESSAGE)
end
end
然后测试通知是否发生。
it 'sends the notifications' do
expect(MyNotifier).to receive(:send)
.with(MyBar.const_get(:NOTIFICATION_MESSAGE))
post my_bars_path, params: { my_bar: valid_attributes }
end
通过send
class 方法,我们不需要使用expect_any_instance_of
。 出于这个原因和许多其他原因,将服务对象编写为没有 state 的类 singleton 是一种常见模式。
这里的缺点是它确实需要了解 MyBar#send_notification 是如何工作的,但如果应用程序使用相同的服务 object 进行通知,这是可以接受的。
或者,创建一个 MyFoo 供其查找。 模拟其创建 MyBar 的调用,确保检查 arguments 是否正确。
let(:foo) {
MyFoo.create!(...)
}
let(:foo_id) { foo.id }
it 'sends the notifications' do
bar = instance_double(MyBar)
expect(bar).to receive(:send_notifications).with(no_args)
allow(foo).to receive_message_chain(:my_bars, :create!)
.with(valid_attributes)
.and_return(bar)
post my_bars_path, params: { foo_id: foo_id, my_bar: valid_attributes }
end
这需要更多的内部知识,并且rubocop-rspec 不喜欢消息链。
或者,模拟MyFoo.find
以返回 MyFoo double。 同样,确保只接受正确的 arguments。
it 'sends the notifications' do
foo = instance_double(MyFoo)
allow(foo).to receive_message_chain(:my_bars, :create!)
.with(valid_attributes)
.and_return(bar)
bar = instance_double(MyBar)
expect(bar).to receive(:send_notifications).with(no_args)
allow(MyFoo).to receive(:find).with(foo.id).and_return(foo)
post my_bars_path, params: { foo_id: foo_id, my_bar: valid_attributes }
end
或者你可以allow(MyFoo).to receive(:find).with(foo_id).and_return(mocked_foo)
,但我发现最好尽可能少地模拟。
在这种情况下,我要做的是使 controller 变“哑”,将持久性和通知逻辑移到服务中,并在集成而不是存根中测试服务。
# controller
def create
my_bar = CreateBar.call params[:foo_id], my_bar_params
redirect_to my_bars_path(my_bar)
end
# service
class CreateBar
def self.call(foo_id, bar_params)
foo = MyFoo.find params[:foo_id]
bar = foo.my_bars.create! bar_params
bar.send_notifications
bar
end
end
# controller spec
it 'creates a new bar' do
bar = double :bar, id: 'whatever'
expect(CreateBar).to receive(:call).with(<attributes>).and_return(bar)
post my_bars_path, params: {my_bar: valid_attribute}
end
it 'redirects to the created bar' do
bar = double :bar, id: 1
allow(CreateBar).to receive(:call).with(<attributes>).and_return(bar)
post my_bars_path, params: {my_bar: valid_attribute}
expect(response).to redirect_to(my_bars_path(1))
end
# service spec
it 'creates a bar' do
# Or just create the object via AR's interface if you don't use factory bot.
foo = FactoryBot.create :foo
CreateBar.call foo.id, <bar_params>
bars = Foo.find(foo.id).bars
expect(bars.count).to eq 1
expect(bars.first).to have_attribute <expected_attributes>
end
it 'sends a bar notification' do
foo = FactoryBot.create :foo
CreateBar.call foo.id, <bar_params>
# NotificationCenter is expected to be called from #send_notification
# and in test environment it records your notifications instead of actually
# sending them.
notifications = NotificationCenter.notifications
expect(notifications.count).to eq <expected notifications count>
# + assert that the notifications have the properties based on the <bar_params>
end
根据 if #send_notifications
的复杂性,您可以将其内联到CreateFoo
中(从而使持久性 model 与业务逻辑分离)或拥有单独的服务SendFooNotifications
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.