我想学习如何使用存根。
class SomeClass
attr_reader :current_user
def initialize(current_user:)
@current_user = current_user
end
def deliver
subscribers.each do |user|
DailyEmail.new(recipient: user).deliver
end
201
end
private
def subscribers
User.all.select(&:email_notifications_enabled?)
end
end
测试DailyEmail从SomeClass调用的新传递方法的正确方法是什么。如果订阅者是活动记录关系,我如何测试每个方法?迭代器之后如何检查状态返回?
我的奇怪解决方案:
RSpec.describe SomeClass do
let(:current_user) { 'user' }
subject { described_class.new(current_user: current_user) }
describe '#deliver' do
let(:subscribers) { ['test2', 'test1'] }
context 'when `each`, `new`, `deliver` methods called in controller `deliver` method' do
it 'calls methods' do
allow(subscribers).to receive(:each)
subscribers.each do |user|
the_double = instance_double(DailyEmail)
expect(DailyEmail).to receive(:new).and_return(the_double).with(recipient: user)
expect(the_double).to receive(:deliver)
expect(subscribers).to have_received(:each)
subject.deliver
end
end
end
end
end
我写了一些东西,但这个实现对我来说很糟糕。我不知道该如何处理迭代器以及如何测试状态。请提供一些提示
这里有一些东西。
首先,您的控制器状态消息是错误的。你现在要做的是返回一个201的身体,而不是状态。它应该返回一个200,你正在执行一个发送电子邮件的操作,而不是创建一个对象。如果您不想返回除肯定错误消息之外的任何其他消息,并且不处理错误消息(您应该这样做(,则应该将201
替换为:render status: 200
你的测试目前并没有真正测试任何东西。如果你的subscribers方法有错误,它不会捕捉到它,如果你的邮件类有错误,你不会捕捉到错误,那又有什么意义呢。
对于控制器中的逻辑,您应该正确地循环发送要发送的邮件,并检查它们是否已发送https://relishapp.com/rspec/rspec-rails/docs/mailer-specs或者将测试一分为二。使用控制器测试快乐路径,并为电子邮件类创建另一个测试。
运行控制器测试的正确方法是使用请求规范,并期望得到正确的响应代码。请注意,如果您在测试数据库中启用了订阅者,它将返回200,如果没有订阅者,它也只返回200。整个代码仍在测试中。如果订阅方法中有错误,它将返回500错误。https://relishapp.com/rspec/rspec-rails/docs/request-specs/request-spec
为了正确地测试控制器测试,您需要在测试数据库中创建对象,然后循环使用它们,而不是像那样模拟它。例如,您可以使用FactoryBot来实现这一点,或者,如果出于某种原因无法添加FactoryBot,您甚至可以截断用户模型;这取决于你在邮差课上做什么。
before :each do
stub_const('User', MockedUserModel)
end
class MockedUserModel < User
def all
arr_of_mocked_users = []
arr_of_mocked_users << User.new(name: 'mocked_user_1', id: 1)
arr_of_mocked_users << User.new(name: 'mocked_user_1', id: 2)
arr_of_mocked_users
end
def email_notifications_enabled?
true
end
end
在当前的实现中有一些奇怪的事情。首先,这个:
class SomeController < ApplicationController
def initialize(current_user:)
@current_user = current_user
end
end
定义一个自定义的initialize
方法,纯粹是为了让测试工作??!
在现代轨道应用程序中测试控制器的标准方法是通过请求规范。(您也可以使用控制器规格,但这通常被认为是较差的,因为您绕过了整个堆栈的关键部分,如路由器。(
其次,这些奇怪的变量:
let(:current_user) { 'user' }
let(:subscribers) { ['test2', 'test1'] }
为什么不使用真正的User
对象??!按照自己的方式进行测试会使测试变得更复杂、更脆弱,也更难推理。
有些人可能强烈倾向于始终将这些测试与数据库解耦,在这种情况下,可以设置类似以下的模拟:
allow(User).to receive(:all).and_return(subscribers)
其他人(包括我(宁愿只在数据库中创建真实的对象,并接受您的测试套件在这里会慢一点。
你不需要在这里使用factory_bot
这样的库,但我推荐它
let(:current_user) { FactoryBot.create(:user) }
let!(:subscribers) { FactoryBot.create_list(:user, 2, email_notifications_enabled: true) }
let!(:non_subscriber) { FactoryBot.create(:user, email_notifications_enabled: false) }
最后,我会在测试中去掉这些部分:
allow(subscribers).to receive(:each)
expect(subscribers).to have_received(:each)
subject.deliver
相反,如果您的测试只是在API上运行端到端场景,并且您正在建立真实的数据库对象,那么您可以确保所有内容都以更全面/更现实的方式进行测试。