【问题标题】:How to isolate a test from external service dependency in RSpec如何在 RSpec 中将测试与外部服务依赖项隔离开来
【发布时间】:2016-07-07 14:24:29
【问题描述】:

设置

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

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 的实际删除。

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

【问题讨论】:

  • 模拟服务对象并存根 usersfind 方法,这样您的条件总是失败?
  • 但是我该如何模拟服务对象呢?以前没有这样做过。

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


【解决方案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

这是一个好方法吗?

【讨论】:

  • 这种方法没有任何问题,但是您并没有直接模拟来自服务的响应,当您尝试测试您的没有在测试中实际调用真正的服务就引发了异常。
  • 你是对的。我确实在不同的背景下遇到了这个问题。您在下面提供的解决方案是可行的方法。谢谢。
【解决方案2】:

你可以这样使用:

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

存根链式方法

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

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

但是,这只有在您不关心它们的调用方式时才真正有用。在这个答案顶部的示例解决方案中,我们测试了正确的用户是否被传递到链中,因此我们从第一个链式方法返回我们的双精度数,以便我们可以对链的第二部分设置期望。

【讨论】:

    猜你喜欢
    • 2013-12-08
    • 1970-01-01
    • 2011-04-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多