问题
I would like to learn how to properly mock object calls inside other classes, foor example I have this controller action:
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
The service class looks like this.
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
I don't want to call the actual Action::PartsShow
service inside this controller action and all other methods, services + the worker because this makes the test very slow. What I want is to test if this service is being called and mock the rest of the services. I don't want to call them in my tests, I want to mock them.
My test looks like this:
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
Could you show me how to properly mock it? I've read about RSpec mocks and stubs but I am confused about it. I would appreciate your help.
回答1:
With rspec-mocks gem, you can use allow_any_instance_of
. Usually, this part lies in before
block.
In fact,Action::PartsShow
is responsible for loading a part, so there is no need to leak two instance methods: call
and part
. You can simplify it through returning the part from 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
Reference
https://relishapp.com/rspec/rspec-mocks/v/3-5/docs/working-with-legacy-code/any-instance
回答2:
Assuming that find_part
calls Part.find(id)
, you can add:
before do
allow(Part).to receive(:find).with(part.id) { part }
end
to ensure that the record lookup always returns your test object. I also suggest reworking your spec a bit:
RSpec.describe PartController, type: :request do
describe 'GET #show' do
let(:request) { get "/api/v1/parts/#{part.id}" }
let(:part) { create(:part) }
it '200s' do
request
expect(response).to have_http_status(:success)
end
end
end
If your project has path helpers, I would also recommend using those instead of the path string.
来源:https://stackoverflow.com/questions/58085529/how-to-properly-mock-itnernal-services-in-rspec