测试中怎么 stub 外部服务


copy from: [How to Stub External Services in Tests
](http://robots.thoughtbot.com/how-to-stub-external-services-in-tests)
如果我们在测试中会访问到外部的服务,往往会有下面的问题:
1. 如果有网络连接问题就会让测试失败
2. 拖慢测试
3. 一般外部的第三方 API 还有访问数限制, 如 twitter
4. 可能你会和队友合作,一起开发,他们的API 还没有开发完成呢,只有一个式样的规范
5. 我们在测试,而第三方的 API 不提供测试环境啊,你怎么测试.

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

#断了网络连接

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

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

```ruby
# spec/spec_helper.rb
require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)

```

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

```ruby
# 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
```

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

```ruby

$ 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 的请求,看看:


```ruby
# 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

```

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

```ruby
$ 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`.

```ruby
# spec/spec_helper.rb
RSpec.configure do |config|
  config.before(:each) do
    stub_request(:any, /api.github.com/).to_rack(FakeGitHub)
  end
end

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

```ruby

# 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` 响应.然后保存在本地文件中

```ruby
# 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 的响应.


```ruby
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`

```ruby
$ rspec spec/features/external_request_spec.rb
.

Finished in 0.04713 seconds
1 example, 0 failures
```
✌️成功了!
这个可以让我们完全推理外部 api 的依赖.
但是在创建一个假的服务的时候,需要注意下面的几件事情:
1. 如果有了这个额额外的服务,就需要额外的维护哦
2. 可能你的这个服务返回的内容过期了哦!

-EOF-