featureスペックでクライアント(JavaScript)側から呼び出すRequestをモックする


Shinosaka.rb Advent Calendar 1日目の記事です。

はじめに

戯言です。本題を見たい人は次へ読み進めることをオススメします。
昨今フロントエンドのフレームワークないし、ライブラリの発展によりフロントエンドとバックエンド別々にテストを実行することが珍しくないと思います。ですが、最終的にはE2Eテストも"したい"、"やらないといけない"と考えた場合に厄介な問題が出てきます。
今回はその問題の1つである外部サイト(外部API)呼び出しのモックについて書きたいと思います。

前提条件

システム構成

まずはシステム構成で以下のような方を対象とした記事です。

SPAで構成する場合はこんなことが往々に発生するのではないでしょうか。(サーバはRailsでなくても可)
この構成時のE2Eテストを実施する場合に、実際に外部APIを叩いてしまっては困ります。サーバ側から外部APIに投げる場合がある場合はwebmockを使用すればよいのですが、今回はそれでは不可能です。そこでpuffing-billyというAPI呼出をモック可能なJavaScriptDriverを使用します。

主な使用フレームワーク(ライブラリ)

Rails:5.0.0.1
capybara:2.10.1
puffing-billy:0.9.1
Reactjs:15.4.1

サンプルプログラム

今回はシステム構成で紹介したプログラムとなるようなサンプルソースをこちらに用意しました。以下に軽く解説をしておきます。

テスト対象の確認

まずはテストするコードを一部抜粋します。

frontend/js/containers/Sample/index.js
import Sample from '../../components/Sample'

class App extends Component {
  constructor(props) {
    super(props)
  }

  render() {
    const { data, actions } = this.props
    return (
      <Sample {...data} onFetchData={actions.getSamples} />
    )
  }
}

まずメインとなるSampleコンテナからSampleコンポーネントへgetSamplesというアクションを渡しています。

frontend/js/components/Sample/index.js
import React, { Component } from 'react'

const Sample = (props) => {
  return(
    <div>
      {props.samples.map((item, i) => <p key={i}>{item}</p>)}
      <button onClick={() => {props.onFetchData()}}>fetch data</button>
    </div>
  )
}

export default Sample

Sampleコンポーネントへ渡されたgetSamplesはボタンをクリックすると動作するようにしています。

frontend/js/actions/sample.js
import { createAction } from 'redux-actions'
import { exampleApi } from '../apis/example'

export const GET_SAMPLES = 'GET_SAMPLES'

export const getSamples = createAction(GET_SAMPLES, exampleApi)

実際のaction(creater)はexampleApiというファンクションを実行しています。

frontend/js/apis/example.js
import request from 'superagent'

export function exampleApi() {
  return request.get('http://example.com/api')
    .then((res) => res.body)
}

exampleApiのファンクションはhttp://example.com/apiというURIをGETで取得しています。

frontend/js/reducers/sample.js
import { handleActions } from 'redux-actions'

export const initialState = {count: 0}

const reducerMap = {
  GET_SAMPLES(state, action) {
    return {...state, samples: action.payload.test}
  },
}

export default handleActions(reducerMap, initialState)

最後に取得したAPIのデータをstateにしています。

コードを見ればわかる通りhttp://example.com/apiなんていうAPIは存在しないものを叩こうとしています

APIモックテスト

それでは実際にpuffing-billyの設定から使用方法を解説します。

インストール

まずは以下のようにGemfileに追加してbundle installします。

Gemfile
group :development, :test do
  gem 'rspec-rails'
end

group :test do
  gem 'capybara'
  gem 'selenium-webdriver'
  gem 'puffing-billy'
end

ちょっと注意なのですが、このpuffing-billyですが、この記事を書いていて気づいたのですが、listenのgemと一緒に動かすと動かないので注意が必要です。(なのでlisten:developmentなので:testに入れてます)

テスト環境設定

インストールしたgemにRSpecが入っているのでまずはそれのgenerateで必要ファイルを生成します。

$ bin/rails g rspec:install

次に生成されたrails_helper.rbに以下の設定を足します。

require 'capybara/rspec'
require 'billy/capybara/rspec'
require 'selenium-webdriver'
Capybara.server_port = 3100
Capybara.app_host = "http://127.0.0.1:3100/"
=begin
Capybara.register_driver :selenium_chrome do |app|
  opts = { browser: :chrome }
  opts[:switches] = ["--no-proxy-server"]
#  opts[:switches] = ["--proxy-server=proxy:3128"]
  Capybara::Selenium::Driver.new app, opts
end
Capybara.default_driver = :selenium_chrome
=end
Capybara.default_driver = :selenium_chrome_billy
ActionController::Base.asset_host = Capybara.app_host

今回は動作が分かるようにわざとSeleniumを入れています。また、firefoxだと面倒な問題が発生するので手間ですが、chromeで動かすようにしています。
chromedriverが必要なのでそれはサイトにいって落としてきて下さい

テストコード

では最後にテストコードを見てみます。

require 'rails_helper'

feature 'Samples spec' do
  scenario 'example.comが2件のデータを返して、表示されること' do
    visit '/samples'
    proxy.stub("http://example.com/api").and_return(
      headers: { 'Access-Control-Allow-Origin'  => '*' },
      json:    {test: [1,2]},
      code:    200,
    )
    click_button 'fetch data'
    expect(all('p').count).to eq 2
  end

  scenario 'example.comが3件のデータを返して、表示されること' do
    visit '/samples'
    proxy.stub("http://example.com/api").and_return(
      headers: { 'Access-Control-Allow-Origin'  => '*' },
      json:    {test: ["a","b","c"]},
      code:    200,
    )
    click_button 'fetch data'
    expect(all('p').count).to eq 3
  end
end

注目してほしいのはproxy.stubの2箇所です。テストするコードにも書かれていましたが、http://example.com/apiのリクエストモックをここで作成しています。これでリクエストがモックされて、設定したJSONが返ってくるようになっています。
サンプルコードをcloneしてbundle exec rspecを動かせば正常にテストが完了します。
実際に動かした場合は以下のように動作するはずです。

12が2つ返ってきた画面表示とabcの3つが返ってきた画面表示になります。

さいごに

いかがでしたでしょうか。もしJavaScript側から外部APIを呼び出すようなシステム構造の場合にpuffing-billyを使用してみてはどうでしょうか。