RSpecにおけるWebmockを使用したstubの使い方

全般

はじめに

初めまして!青柳です。30代で未経験ながらエンジニアに転職し、早2年が経ちます。以前よりRuby on Railsを勉強していましたが、最近は業務で携わることも増えてきました。基本的なソースコードを書くことは慣れてきましたが、RSpecを書く機会があったときにつまずきました。

今回はそのRSpecの中でも初めて使用したstubについてまとめました。

Webmockとは

HTTPリクエストするときに、リクエスト部分をスタブ化するためのライブラリのことです。
一から実装するのではなくGemを使用することで比較的簡単に実装することが可能になります!

Webmockを使用する準備

まずはgemを追加していきます。主にテストで使用するのでtest配下に追加します。(プログラムによってはdevelopmentなどへの追加もあり得ます)

#Gemfile

group :test do
  gem "webmock"
end

その後、installを実行します。

$ bundle install

テストで使用するModel

今回はBitflyer APIを叩く簡素なModelを定義します。

# model/Bitflyer.rb

require 'net/https'
require 'uri'

class Bitflyer < ApplicationRecord

  class << self
    def access(params)
      begin
        uri = URI('https://api.bitflyer.jp/v1/board')
        uri.query = params
        response = Net::HTTP.get_response(uri)
        puts response.code
      rescue => e
        puts e
      end
    end
  end
end

Bitflyer.accessとするとhttps://api.bitflyer.jp/v1/boardに対してアクセスをします。
引数にparamsを指定すれば、アクセス先を指定することもできます。
また例外が発生したときにキャッチできるようにrescueを追加しています。

RSpecを実装する

では実際にRSpecを実装します。
まず初めにゴールとなるソースをご紹介します。

#spec/bitflyer_spec.rb

require 'rails_helper'
require 'webmock/rspec'

RSpec.describe Bitflyer do
  describe "#access" do
    let(:endpoint) { {params: 'product_code=FX_BTC_JPY'} }
    subject { Bitflyer.access(endpoint[:params]) }

    context 'Bitflyer Api' do
      before do
        bitflyer_api_request
      end

      it 'should be access bitflyer api'do
        expect{ subject }.not_to raise_error
      end
    end
  end

  def bitflyer_api_request
    WebMock.stub_request(:get, "https://api.bitflyer.jp/v1/board?#{endpoint[:params]}")
    .to_return(
      body: File.read("#{Rails.root}/test/fixtures/bitflyer/response.json"),
      status: 'OK',
      headers: { 'Content-Type' => 'application/json'}
    )
  end
end

では解説していきます。

まず、webmockを使用するために、ソース上部で読み込みます。

require 'webmock/rspec'

次にsubjectの実装です。
下記のように先ほど実装したBitflyerモデルのaccessメソッドを実行するようにsubjectを登録しています。
また今回は引数にendpoint[:params]を指定しています。
letで登録している変数を参照しており、product_code=FX_BTC_JPYが代入されます。
これはBTCとJPYの為替情報を取得するAPIになります。(先程の Bitflyerモデルのaccessメソッドを確認していただければと思います)

let(:endpoint) { {params: 'product_code=FX_BTC_JPY'} }
subject { Bitflyer.access(endpoint[:params]) }

次にcontext Bitflyer Apiの中のbeforeの中身を見てみましょう。

context 'Bitflyer Api' do
  before do
    bitflyer_api_request
  end
・
・
・

この bitflyer_api_requestがWebmock登録箇所になります。
実装の中身を見ていきましょう。

def bitflyer_api_request
  WebMock.stub_request(:get, "https://api.bitflyer.jp/v1/board?#{endpoint[:params]}")
  .to_return(
      body: File.read("#{Rails.root}/test/fixtures/bitflyer/response.json"),
      status: 'OK',
      headers: { 'Content-Type' => 'application/json'}
   )
end

上記のようにまずstubを登録するにはWebMock.stub_requestメソッドを使用します。
引数にHTTPメソッド(get、post、patchなど)と、登録したいURLを設定します。

またそれに対してto_returnメソッドを指定するとresponseを指定することもできます。
上記では、response.jsonというファイルを指定しています。
こちらには実際にAPIにアクセスした際に期待するレスポンスを記入しておくことで、(今回は行いませんが)正しいレスポンスが得られているかなどのテストに使用するができます。

最後に実際のテストですが、今回は下記のように実装しています。
こちらはsubjectを実行した際にerrorが発生しないことを確かめるテストになります。

it 'should be access bitflyer api'do
  expect{ subject }.not_to raise_error
end

これでRSpecを実行すると登録されたstubを元にテストが開始されます!

注意点

前提として、テストの実行前にstubを登録しないと、Bitflyer.accessの中のresponse = Net::HTTP.get_response(uri)が実際にuriにアクセスしてしまいます。
そのため、今回のケースのように、テスト実行前にbeforeで登録しておくことが重要です。

また、stubに登録しているURLやHTTPメソッドが間違っている場合ももちろん動作しません。
RSpecを実行してみて、エラーが出るとき、もしそのエラーが実際にAPIにアクセスしているようであれば、stubのURLやHTTPメソッドの指定がうまくいっていないケースが多いです。

Webmockを使用してStubを登録した際の流れとしては下記のようになります。

  • Stubを登録する
  • テストを実行する
  • テストの中であるURLに対してアクセスをする
  • Stubに登録されたURLと照合して合致すればStubに登録された情報を元にレスポンスを返す(実際のURL先にアクセスはしない)
  • もし合致しなければそのまま実際のURLにアクセスする

この流れを覚えておけば、スムーズに実装できると思います。

まとめ

今回はRSpecにおけるWebmockの使用方法についてご紹介しました。
私がこの機能を使用したのは、業務にて外部API連携を実装していて、そのテスト実装に使用しました。
具体的にはAPIが返す値を設定できるのですが、その設定が遅延していたため、stubを使用してシステムを先に構築しようと言うことになったために実装するというものでした。
レアケースかもしれませんが、外部APIのテストなどに使用するケースはあると思います。
皆さんの業務の一助になれば幸いです。

初めての投稿で拙いところもあったと思いますが、今後も繰り返し投稿していきますのでよろしくお願い致します。

コメント