TDD
RSpec
Mock
stub

测试中怎么 stub 外部服务

More than 3 years have passed since last update.

copy from: How to Stub External Services in Tests



如果我们在测试中会访问到外部的服务,往往会有下面的问题:

1. 如果有网络连接问题就会让测试失败

2. 拖慢测试

3. 一般外部的第三方 API 还有访问数限制, 如 twitter

4. 可能你会和队友合作,一起开发,他们的API 还没有开发完成呢,只有一个式样的规范

5. 我们在测试,而第三方的 API 不提供测试环境啊,你怎么测试.

当我们的程序和外部交互的时候,我们需要确保我们的测试 case 没有访问到第三方的服务.我们的测试应该是隔离的.


断了网络连接

我们要使用Webmock,可以用来stub外部http requestgem, 在这个例子里,我们需要通过Github API来搜索FactoryGirl 的贡献者!

首先,我们要在spec_helper.rb里禁用外部连接. 这样的:

# spec/spec_helper.rb

require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)

现在来看看是否会有异常:

# spec/features/external_request_spec.rb

require 'spec_helper'

feature 'External request' do
it 'queries FactoryGirl contributors on GitHub' do
uri = URI('https://api.github.com/repos/thoughtbot/factory_girl/contributors')

response = Net::HTTP.get(uri)

expect(response).to be_an_instance_of(String)
end
end

这个的情况下,我们应该看见这些预期的异常:


$ rspec spec/features/external_request_spec.rb
F

Failures:

1) External request queries FactoryGirl contributors on GitHub
Failure/Error: response = Net::HTTP.get(uri)
WebMock::NetConnectNotAllowedError:
Real HTTP connections are disabled.
Unregistered request: GET https://api.github.com/repos/thoughtbot/factory_girl/contributors
with headers {
'Accept'=>'*/*',
'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
'Host'=>'api.github.com',
'User-Agent'=>'Ruby'
}

You can stub this request with the following snippet:

stub_request(:get, "https://api.github.com/repos/thoughtbot/factory_girl/contributors").
with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Host'=>'api.github.com', 'User-Agent'=>'Ruby'}).
to_return(:status => 200, :body => "", :headers => {})

============================================================
# ./spec/features/external_request_spec.rb:8:in `block (2 levels) in <top (required)>'

Finished in 0.00499 seconds
1 example, 1 failure

我们怎么处理这个情况呢,可以通过 stub 这个 api 的请求,看看:

# spec/spec_helper.rb

RSpec.configure do |config|
config.before(:each) do
stub_request(:get, /api.github.com/).
with(headers: {'Accept'=>'*/*', 'User-Agent'=>'Ruby'}).
to_return(status: 200, body: "stubbed response", headers: {})
end
end

现在再次跑这个测试看看:

$ rspec spec/features/external_request_spec.rb

.

Finished in 0.01116 seconds
1 example, 0 failures


VCR

另外一个方式就是为了防止外部的请求,可以通过录制一个API 的访问,然后当再次有 API 的请求时就把结构回放给调用方, 这个VCR gem 有一个cassettes 的概念,他能够录制测试 case访问外部的http 请求,然后以供后需.

当你的使用 VCR 的时候你需要注意下面的方式:


  1. 和其他人沟通怎么共享这个 cassette

  2. 第一次访问这个外部请求需成功

  3. 很难模拟失败

接下来,我们要创建一个假版本的Github 服务.


创建一个假的服务(用 sinatra)

当你的程序严重依赖第三方服务的时候,需要考虑通过sinatra 来在你的程序中创建一个假的服务.它会让你的测试完全隔离起来.而且可以很容易控制测试的响应时间.

首先,我们通过Webmock 来把所有的请求路由到这个我们新建的Sinatra 程序里-- FakeGithub.

# spec/spec_helper.rb

RSpec.configure do |config|
config.before(:each) do
stub_request(:any, /api.github.com/).to_rack(FakeGitHub)
end
end

我们来创建这个假的服务;


# spec/support/fake_github.rb
require 'sinatra/base'

class FakeGitHub < Sinatra::Base
get '/repos/:organization/:project/contributors' do
json_response 200, 'contributors.json'
end

private

def json_response(response_code, file_name)
content_type :json
status response_code
File.open(File.dirname(__FILE__) + '/fixtures/' + file_name, 'rb').read
end
end

下载这个例子的json 响应.然后保存在本地文件中

# spec/support/fixtures/contributors.json

[
{
"login": "joshuaclayton",
"id": 1574,
"avatar_url": "https://2.gravatar.com/avatar/786f05409ca8d18bae8d59200156272c?d=https%3A%2F%2Fidenticons.github.com%2F0d4f4805c36dc6853edfa4c7e1638b48.png",
"gravatar_id": "786f05409ca8d18bae8d59200156272c",
"url": "https://api.github.com/users/joshuaclayton",
"html_url": "https://github.com/joshuaclayton",
"followers_url": "https://api.github.com/users/joshuaclayton/followers",
"following_url": "https://api.github.com/users/joshuaclayton/following{/other_user}",
"gists_url": "https://api.github.com/users/joshuaclayton/gists{/gist_id}",
"starred_url": "https://api.github.com/users/joshuaclayton/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/joshuaclayton/subscriptions",
"organizations_url": "https://api.github.com/users/joshuaclayton/orgs",
"repos_url": "https://api.github.com/users/joshuaclayton/repos",
"events_url": "https://api.github.com/users/joshuaclayton/events{/privacy}",
"received_events_url": "https://api.github.com/users/joshuaclayton/received_events",
"type": "User",
"site_admin": false,
"contributions": 377
}
]

更新测试,验证是不是返回了预期 stub 的响应.

require 'spec_helper'

feature 'External request' do
it 'queries FactoryGirl contributors on GitHub' do
uri = URI('https://api.github.com/repos/thoughtbot/factory_girl/contributors')
# 看好,这里是 JSON.load 坑爹!!!
response = JSON.load(Net::HTTP.get(uri))

expect(response.first['login']).to eq 'joshuaclayton'
end
end

跑这个specs

$ rspec spec/features/external_request_spec.rb

.

Finished in 0.04713 seconds
1 example, 0 failures

✌️成功了!

这个可以让我们完全推理外部 api 的依赖.

但是在创建一个假的服务的时候,需要注意下面的几件事情:

1. 如果有了这个额额外的服务,就需要额外的维护哦

2. 可能你的这个服务返回的内容过期了哦!

-EOF-