Day 25 用 WebMock + VCR 来实作测试

该文章同步发布於:我的部落格

WebMock 以及 VCR 是拿来实作关於网站请求的工具,在这篇文章中也会解释两者分别是什麽,并且做一个简单的教学!

为什麽需要 WebMock & VCR ?

测试的原则之一就是测试的确定性,一个测试的通过以及失败都应该是应用程序的程序码内容来决定,而不是其他的因素,所有的测试都理应要通过,无论他是用什麽方式运行,在什麽时间运行,或是其他的因素。

由於这个原因,我们就必须在封装的世界中运行测试,如果我们想要测试的确定性以及稳定性,我们就不该让测试和网路对话,因为网路有很多的变因可能改变测试的结果。

想像一下程序里面和第三方 API 的交流,在想像一下每次的测试都会去触碰这个 API,在想像一下,某一次的测试运行中,第三方的 API 碰巧坏掉了,导致测试的失败,但这次的失败明显是不合逻辑的,因为测试要做的目的是要告诉我们的程序码有问题,但这次不是,是外部的干扰导致而成!

如果我们的测试不想和真实的网路对话,那我们就需要一种方法来模拟和网路的互动,让我们的应用程序在一个稳定的环境中进行测试,这就是 VCR & WebMock 为什麽会存在的原因!

我们也不希望测试的过程中,真实的对资料有任何的变动,假设我们写了一个删除用户的测试,总不希望测试的过程中真的影响了资料吧?因此,VCR & WebMock 也给了我们不必接触真实资料的功能!

两者之间的不同

VCR 是一个拿来纪录你的应用程序和 HTTP 交流过程的工具,并且在之後进行回放,而且不需要写什麽程序码,VCR 欺骗你的应用程序,让他以为他正在接受来自网路的回覆,但实际上你的专案只是在接受预录的 VCR 数据而已!

WebMock 则不像 VCR 那样可以纪录和回放,尽管 HTTP 的交流是可以被伪造的,但那和 VCR 还是有所不同,WebMock 需要写更多的程序码,而且也有诸多的限制,在这次的文章中,会有很基本的介绍和了解!

该怎麽做?

这篇文章将会做一个简单的例子,说明如何使用 VCR & WebMock 来满足需求,执行对於网路的请求,但不实际的发送请求出去!

至於这篇文章的基础 Rails Setup 就不展示了,主要专注在工具的使用上面!

情境

我们要写一个小型的搜寻功能,去请求第三方的 API,所以在测试中,我们也该发送出 API 来完成测试

WebMock

一但测试完成的差不多了,我们就会安装和设定 WebMock,这时候会禁止任何的网路请求,因此,我们的测试会停止运作。

VCR

最後,我们会安装和设定 VCR,VCR 能够和 WebMock 做互动,也因为这样,VCR & WebMock 可以一起合作,在某些条件下,我们的测试可以进入网路,VCR 将纪录测试过程的 HTTP 请求及回覆,接着在之後的测试中,都会使用 VCR 的回放,而不是运作都发送新的请求!

功能实作

因为临时想不到有什麽第三方的资源可以去打,就用想像的方式来进行~

今天这篇文章实作的功能是一个搜寻某些第三方资料的功能,使用者可以输入姓名,点击搜寻,然後会有资料出现!

下面是 Controller 的程序码:

# app/controllers/data_searches_controller.rb
ENDPOINT_URL = "https://xxxx/api"

class DataSearchesController < ApplicationController
  def new
    @results = []
    return unless params[:first_name].present? || params[:last_name].present?

    query_string = {
      first_name: params[:first_name],
      last_name: params[:last_name]
    }.to_query

    uri = URI("#{ENDPOINT_URL}/?#{query_string}")
    response = Net::HTTP.get_response(uri)
    @results = JSON.parse(response.body)["results"]
  end
end

我们也应该要有 View 来 render 画面:

<%= form_tag xxx_search_path, method: :get do %>
  <%= text_field_tag :first_name, params[:first_name] %>
  <%= text_field_tag :last_name, params[:last_name] %>
  <%= submit_tag "搜寻" %>
<% end %>

<% @results.each do |result| %>
  <div>
    <%= result["basic"]["first_name"] %>
    <%= result["basic"]["last_name"] %>
    <%= result["number"] %>
  </div>
<% end %>

写测试

简单的写一个 feature test,在姓名的栏位分别填入 "Robert" & "Chang",点击搜寻,然後期待 Robert Chang 的某个识别证号码会出现在画面上!

# spec/feature/data_search_spec.rb

require "rails_helper"

RSpec.feature "Data Search", type: :feature do
  scenario "show the identify number" do
    visit data_search_path
    fill_in "first_name", with: "Robert"
    fill_in "last_name", with: "Chang"
    click_on "搜寻"

    # 这段是 RobertChang 的识别码
    expect(page).to have_content("118583921")
  end
end

OK,这样会通过,没有问题!

安装以及设定 WebMock

像刚刚前面提过的,我们不希望测试时也真的送出第三方的 API 请求,所以我们需要假造一段请求。

我们先把 WebMock 装入 Gemfile:

group :development, :test do
  gem 'webmock', '~> 3.14'
end

第二步,我们建立一个档案在 spec/support/webmock.rb,并且加入下面的程序码:

# spec/support/webmock.rb

# This line makes it so WebMock and RSpec know how to talk to each other.
require "webmock/rspec"

# This line disables HTTP requests, with the exception of HTTP requests
# to localhost.
WebMock.disable_net_connect!(allow_localhost: true)

记得到 spec/rails_helper.rb 里面去 uncomment 下面那一行,不然 spec/support 的文件不会被 Load 进来用。

# spec/rails_helper.rb

# Make sure to uncomment this line
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }

第三步:看测试坏掉!

安装之後再跑了一次测试,他会坏掉,然後说 Real HTTP connections are disabled,还给我们一些提示,叫我们可以去 Stub 这个请求,但我们不这麽做,我们要用 VCR 来替代!

Failures:

  1) Data Search show the indentify number
   Failure/Error: response = Net::HTTP.get_response(uri)

   WebMock::NetConnectNotAllowedError:
     Real HTTP connections are disabled. Unregistered request: GET https://xxx/api/?first_name=Robert&last_name=Chang with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Host'=>'xxx', 'User-Agent'=>'Ruby'}

     You can stub this request with the following snippet:

     stub_request(:get, "https://xxx/api/?first_name=Robert&last_name=Chang").
       with(
         headers: {
           'Accept'=>'*/*',
           'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
           'Host'=>'xxx',
           'User-Agent'=>'Ruby'
           }).
         to_return(status: 200, body: "", headers: {})

       ============================================================

安装以及设定 VCR

一样先加入 Gemfile:

# Gemfile

group :development, :test do
  gem 'vcr', '~> 6.0'
end

接着我会加入这个设定档,我有加入了自己的注解,这样可以更快理解!

# spec/support/vcr.rb

VCR.configure do |c|
  # 这个资料夹是存放 VCR 的地方,就是存放和 HTTP 纪录的过程
  c.cassette_library_dir = "spec/cassettes"

  # 这行让 VCR 和 WebMock 知道如何和彼此沟通!
  c.hook_into :webmock

  # 这行让 VCR 忽略了对 localhost 的请求,这是必要的尽管 WebMock allow_localhost 是 true
  c.ignore_localhost = true

  # ChromeDriver 会向 chromedriver.storage.googleapis.com 发送 update 的请求。
  # 这些请求会在我们的 cassette 中发出噪音,除非我们告诉 VCR 去忽略他
  c.ignore_hosts "chromedriver.storage.googleapis.com"
end

加入 VCR 罗!

现在我们可以加入 VCR.use_cassette "data_search" 的 block 进入测试中,data_search 是可以任意取的名字,只是让 VCR 可以辨识。

# spec/feature/data_search_spec.rb

require "rails_helper"

RSpec.feature "Data Search", type: :feature do
  scenario "show the identify number" do
    VCR.use_cassette "data_search" do # <----- 加这行
      visit data_search_path
      fill_in "first_name", with: "Robert"
      fill_in "last_name", with: "Chang"
      click_on "搜寻"

      # 这段是 RobertChang 的识别码
      expect(page).to have_content("118583921")
    end
  end
end

上次在只有安装 WebMock 的时候喷错了,因为 WebMock 阻止了 HTTP 的请求,但现在执行这个测试会通过,因为 VCR 和 WebMock 一起让 HTTP 请求发生。

结束会去看看 spec/cassettes 资料夹,会发现有一个叫做 data_search.yml 的档案,内容如下:

---
http_interactions:
- request:
    method: get
    uri: https://xxx/api/?first_name=Robert&last_name=Chang
    body:
      encoding: US-ASCII
      string: ''
    headers:
      Accept-Encoding:
      - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
      Accept:
      - "*/*"
      User-Agent:
      - Ruby
      Host:
      - xxx
  response:
    status:
      code: 200
      message: OK
    headers:
      Date:
      - Fri, 18 Oct 2021 03:02:41 GMT
      Content-Type:
      - application/json
      Strict-Transport-Security:
      - max-age=31536000; includeSubDomains
      Set-Cookie:
      - TS017b4e40=01acfeb9489bd3c233ef0e8a55b458849e619bdc886c02193c4772ba662379fa1f8493887950c06233f28bbbaac373afba8b58b00f;
        Path=/; Domain=xxx
      Transfer-Encoding:
      - chunked
    body:
      encoding: UTF-8
      string: '{ 回传的资料... }'
  recorded_at: Fri, 18 Oct 2021 03:02:41 GMT
recorded_with: VCR 6.0.0

接着在之後的每次测试,VCR 都会问,有没有一个叫做 data_search 的录影带,没有的话就允许去执行一次 HTTP 的请求,有的话就使用它!

小结

今天花了蛮多的时间写这个的,内容都是根据之前自己玩的 sideproject 改编内容而成~

所以 VCR 中的回传内容应该会是一些 json 格式的资料,但我相信理解概念就可以了,和实际运作的 Code 没有太大的关系!

感谢收看~


<<:  DAY24 - 利用 uptime 让你的 Heroku 永不休眠

>>:  Day 0x 1D - odoo addons 永丰金流开发(Part 4 - Website template, data... more)

Day 12 (Ai)

2021版本才能存云端文件 => 可以版本控制 =>视窗 =>版本纪录 1.基础...

今晚来聊聊铼德2349

晚上在看线图的时候,发现铼德2349有几个观点可以提出来跟大家分享。 在今年1月初时,跌破季线,这时...

第42天~

这个得上一篇:https://ithelp.ithome.com.tw/articles/10258...

帮忙填写「资讯相关」问卷

#来抽Airpods Pro, Marshall喇叭, 空气清净机, 全联/7-11/Starbuc...

Day 14 | 同步与非同步- Handler类别

Handler类别 使不同Thread沟通的机制,将Thread要传递的讯息放入Message类别,...