Reputation: 629
In one of the controller actions we have an external service dependency like this:
def delete
@user = User.find(params[:id])
# Fail if the user is registered on SomeService
external = SomeService::Client.new(app_id: SOME_ID, api_key: SOME_KEY)
if external.users.find(user_id: @user.id)
fail Error::SomeService::ResourcePresent
end
@user.destroy!
render nothing: true, status: :ok
end
And a very simple spec that tests the action:
context "#delete" do
it "should return the correct success status" do
user = User.create!(email: "[email protected]", password: "12345678")
delete :delete, id: user.id
expect(response).to have_http_status(:ok)
end
end
I would like to isolate the controller action from its dependency on SomeService
and only test the actual deletion of the user
.
What's a good approach for bypassing the external so my tests can pass?
Upvotes: 2
Views: 993
Reputation: 10898
You could use something like this:
context "#delete" do
before(:each) do
# First of all, you'll want to mock out the service and return your mocked service instance
service_double = double # This is the mocked object you want to control
some_service_client = class_double("SomeService::Client") # This mocks the class that you're calling
expect(some_service_client).to receive(:new).and_return(service_double) # This stubs the class method and returns your mocked object
end
it "should return the correct success status" do
# Create your user as usual
user = User.create!(email: "[email protected]", password: "12345678")
# We'll just return the same mocked object from the chained method
expect(service_double).to receive(:users).and return(service_double)
# And we'll test the correct user was passed to the service, and control the response
expect(service_double).to receive(:find).with({userid: user.id}).and return(false)
delete :delete, id: user.id
expect(response).to have_http_status(:ok)
end
end
RSpec has a handy solution for this, which you could use here. It goes something like this:
service_double.stub_chain(:users, :find).and_return(true)
However, this is only really useful if you don't care how they were called. In the example solution at the top of this answer we test that the correct user was passed into the chain, and hence we return our double from the first chained method so that we can set expectations on the second part of the chain.
Upvotes: 2
Reputation: 629
Extract the dependency into a separate method and stub it like this:
def delete
@user = User.find(params[:id])
fail_if_present_on_some_service(@user)
@user.destroy!
render nothing: true, status: :ok
end
def fail_if_present_on_some_service(user)
external = SomeService::Client.new(app_id: SOME_ID, api_key: SOME_KEY)
if external.users.find(user_id: @user.id)
fail Error::SomeService::ResourcePresent
end
end
And stubbing this method inside a before
block like this:
context "#delete" do
before :context do
allow(controller).to receive(:fail_if_present_on_some_service) { true }
end
it "should return the correct status" do
user = User.create!(email: "[email protected]", password: "12345678")
delete :delete, id: user.id
expect(response).to have_http_status(:ok)
end
end
Is this a good approach?
Upvotes: 1