如何在RSpec中将测试与外部服务依赖关系隔离开来

时间:2016-03-21 07:03:25

标签: ruby-on-rails rspec mocking dependencies isolation

设置

在其中一个控制器操作中,我们有一个外部服务依赖项,如下所示:

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

一个非常简单的测试动作的规范:

context "#delete" do
  it "should return the correct success status" do
    user = User.create!(email: "john@smith.com", password: "12345678")

    delete :delete, id: user.id
    expect(response).to have_http_status(:ok)
  end
end

问题

我想将控制器操作与其对SomeService的依赖关系进行隔离,并仅测试user的实际删除。

什么是绕过外部的好方法,以便我的测试可以通过?

2 个答案:

答案 0 :(得分:1)

可能的解决方案

将依赖项提取到一个单独的方法中并将其存储为:

控制器:

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

并在before块中对此方法进行存根,如下所示:

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: "john@smith.com", password: "12345678")

    delete :delete, id: user.id
    expect(response).to have_http_status(:ok)
  end
end

这是一个好方法吗?

答案 1 :(得分:1)

您可以使用以下内容:

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: "john@smith.com", 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

Stubbing Chained Methods

RSpec有一个方便的解决方案,你可以在这里使用。它是这样的:

service_double.stub_chain(:users, :find).and_return(true)

但是,如果你不关心它们的调用方式,这只是非常有用。在本答案顶部的示例解决方案中,我们测试正确的用户是否被传递到链中,因此我们从第一个链接方法返回我们的double,以便我们可以在链的第二部分设置期望。