如何在 RSpec 中正确模拟内部服务?
How to properly mock itnernal services in RSpec?
我想学习如何在其他 classes 中正确模拟对象调用,例如我有这个控制器操作:
def show
service = Action::PartsShow.new(show_params, current_user)
service.call
render json: service.part, root: :part, serializer: PartSerializer, include: '**',
scope: {current_user: current_user}
end
服务 class 看起来像这样。
module Action
class PartsShow < PartsShowBase
def find_part
...
end
end
end
module Action
class PartsShowBase
attr_reader :part
def initialize(params, current_user)
@params = params
@current_user = current_user
end
def call
find_part
reload_part_availability
reload_part_price if @current_user.present?
end
private
def reload_part_availability
ReloadPartAvailabilityWorker.perform_async(part.id)
end
def reload_part_price
ExternalData::LauberApi::UpdatePrices.new(@current_user, [part]).execute
end
end
end
我不想在这个控制器操作和所有其他方法、服务 + 工作人员中调用实际的 Action::PartsShow
服务,因为这会使测试非常慢。我想要的是测试是否正在调用此服务并模拟其余服务。我不想在我的测试中调用它们,我想模拟它们。
我的测试是这样的:
RSpec.describe PartController, type: :request do
describe 'GET #show' do
let(:part) { create(:part) }
subject { get "/api/v1/parts/#{part.id}" }
expect(response_body).to eq(200)
# ...
end
end
你能告诉我如何正确模拟它吗?我读过 RSpec 模拟和存根,但我对此感到困惑。非常感谢您的帮助。
有了rspec-mocks gem,就可以使用allow_any_instance_of
。通常,这部分位于 before
块中。
其实Action::PartsShow
负责加载一部分,所以不需要泄漏call
和part
两个实例方法。您可以通过从 call
.
返回部分来简化它
module Action
class PartsShowBase
#attr_reader :part
def call
find_part # assign @part
reload_part_availability
reload_part_price if @current_user.present?
@part
end
...
end
RSpec.describe PartController, type: :request do
before :all do
allow_any_instance_of(Action::PartsShow).to receive(:call).and_return(returned_part)
end
参考
https://relishapp.com/rspec/rspec-mocks/v/3-5/docs/working-with-legacy-code/any-instance
假设find_part
调用Part.find(id)
,你可以添加:
before do
allow(Part).to receive(:find).with(part.id).and_return(part)
end
确保记录查找总是returns你的测试对象。我还建议稍微修改一下您的规范:
RSpec.describe PartController, type: :request do
subject { response }
describe '#show' do
let(:request) { get api_v1_part_path(part.id) }
# If you insist on mocking the object, might as well use build_stubbed
let(:part) { build_stubbed(:part) }
let(:json) { JSON.parse(response.body).deep_symbolize_keys }
let(:expected) {
{
part: {
id: parts.id,
# ...
}
}
}
before do
# This is the recommended way to mock an object
allow(Part).to receive(:find).with(part.id).and_return(part)
request
end
# Validate response status
# https://relishapp.com/rspec/rspec-rails/docs/matchers/have-http-status-matcher
# If you are using shoulda matchers - it works bc subject is the response
it { should have_http_status(:success) }
# otherwise
it { expect(response).to have_http_status(:success) }
# Validate response body
it { expect(json).to eq(expected) }
end
end
如果您的项目有 path helpers,我也建议使用这些而不是路径字符串。
我想学习如何在其他 classes 中正确模拟对象调用,例如我有这个控制器操作:
def show
service = Action::PartsShow.new(show_params, current_user)
service.call
render json: service.part, root: :part, serializer: PartSerializer, include: '**',
scope: {current_user: current_user}
end
服务 class 看起来像这样。
module Action
class PartsShow < PartsShowBase
def find_part
...
end
end
end
module Action
class PartsShowBase
attr_reader :part
def initialize(params, current_user)
@params = params
@current_user = current_user
end
def call
find_part
reload_part_availability
reload_part_price if @current_user.present?
end
private
def reload_part_availability
ReloadPartAvailabilityWorker.perform_async(part.id)
end
def reload_part_price
ExternalData::LauberApi::UpdatePrices.new(@current_user, [part]).execute
end
end
end
我不想在这个控制器操作和所有其他方法、服务 + 工作人员中调用实际的 Action::PartsShow
服务,因为这会使测试非常慢。我想要的是测试是否正在调用此服务并模拟其余服务。我不想在我的测试中调用它们,我想模拟它们。
我的测试是这样的:
RSpec.describe PartController, type: :request do
describe 'GET #show' do
let(:part) { create(:part) }
subject { get "/api/v1/parts/#{part.id}" }
expect(response_body).to eq(200)
# ...
end
end
你能告诉我如何正确模拟它吗?我读过 RSpec 模拟和存根,但我对此感到困惑。非常感谢您的帮助。
有了rspec-mocks gem,就可以使用allow_any_instance_of
。通常,这部分位于 before
块中。
其实Action::PartsShow
负责加载一部分,所以不需要泄漏call
和part
两个实例方法。您可以通过从 call
.
module Action
class PartsShowBase
#attr_reader :part
def call
find_part # assign @part
reload_part_availability
reload_part_price if @current_user.present?
@part
end
...
end
RSpec.describe PartController, type: :request do
before :all do
allow_any_instance_of(Action::PartsShow).to receive(:call).and_return(returned_part)
end
参考
https://relishapp.com/rspec/rspec-mocks/v/3-5/docs/working-with-legacy-code/any-instance
假设find_part
调用Part.find(id)
,你可以添加:
before do
allow(Part).to receive(:find).with(part.id).and_return(part)
end
确保记录查找总是returns你的测试对象。我还建议稍微修改一下您的规范:
RSpec.describe PartController, type: :request do
subject { response }
describe '#show' do
let(:request) { get api_v1_part_path(part.id) }
# If you insist on mocking the object, might as well use build_stubbed
let(:part) { build_stubbed(:part) }
let(:json) { JSON.parse(response.body).deep_symbolize_keys }
let(:expected) {
{
part: {
id: parts.id,
# ...
}
}
}
before do
# This is the recommended way to mock an object
allow(Part).to receive(:find).with(part.id).and_return(part)
request
end
# Validate response status
# https://relishapp.com/rspec/rspec-rails/docs/matchers/have-http-status-matcher
# If you are using shoulda matchers - it works bc subject is the response
it { should have_http_status(:success) }
# otherwise
it { expect(response).to have_http_status(:success) }
# Validate response body
it { expect(json).to eq(expected) }
end
end
如果您的项目有 path helpers,我也建议使用这些而不是路径字符串。