0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Rspecでモックを使ったテストを書こうの巻(テスト書きづらいコードを少し改善することを添えて

Posted at

この記事で書くこと

  • 最初のコードからどのように変化したか。
  • モックのテストが苦手な人間がどのようにテストを乗り越えたか
    • double
    • allow_any_instance_of
    • allow

この記事で書かないこと

  • 使用しているgemの詳しい説明
  • Amazon Product Advertising API 5.0. に関する詳しいapi仕様
  • rakeタスクのspecの良い(適した、or正しい)書き方

一番伝えたい事

  • テストを書く事は、より良いコードを書く為の近道 :island:

テストしようと思ったコード

rakeタスクです。 コードは実際のものと比べて簡略化しています。
Amazon Product Advertising API 5.0. をリクエストして商品を作成する、という内容。
今現在(2020/07)Ruby向けの公式SDKはない為、
https://github.com/hakanensari/vacuum
今回はvacuumというgemを使用しての実装になります。

lib/tasks/item.rake
namespace :item do
  desc 'amazon apiで取得した商品を登録する'
  task insert_from_amazon_search_item: :environment do
    # request作成
    request = Vacuum.new(marketplace: 'JP',
                         access_key: 'xxxxxxxxxx',
                         secret_key: 'yyyyyyy',
                         partner_tag: 'hoge_fuga')
    # search_items を呼び出す
    response = request.search_items({
      search_index: 'All',
      market_place: 'www.amazon.co.jp',
      resources: [
        'ItemInfo.Title',
        'Offers.Summaries.LowestPrice'
      ],
      keywords: 'ねこまみれ'
    })&.to_h

    response['SearchResult']['Items'].each do |item|
      new_price = item['Offers']['Summaries'].detect do |summary|
        summary['Condition']['Value'] == 'New'
      end

      # itemの作成
      Item.create name: item['ItemInfo']['Title']['DisplayValue'], price: new_price['LowestPrice']['Amount']
    end
  end
end
spec/lib/tasks/item_spec.rb

require 'rails_helper'
require 'rake'

RSpec.describe 'item:insert_from_amazon_search_item' do
  before(:all) do
    @rake = Rake::Application.new
    Rake.application = @rake
    Rake.application.rake_require 'tasks/item'
    Rake::Task.define_task(:environment)
  end

  before(:each) do
    @rake[task_name].reenable
  end

  context 'when valid response returns,' do
    let(:task_name) { 'item:insert_from_amazon_search_item' }
    it 'creates new item.' do
      # ど、どうしたらいい・・・・・・
      expect do
        @rake[task_name].invoke
      end.to change(Item, :count).by(1)
    end
  end
end

自分がテストを書くのが難しいと感じた部分

  • search_items は Vacuum の インスタンスに対して実行できるメソッド
  • その search_items からitemを参照するために使用する to_hArray#to_h ではなく、
    https://github.com/hakanensari/vacuum/blob/master/lib/vacuum/response.rb#L44
    Vacuum::Response に定義されているメソッド。どうテストでは答えるようにしたらいい?

難しいと感じるのは、コードの書き方がおかしいのではと気づく(自分のテストを書く能力が低いのは勿論あるが)

ロジックを修正していくにあたり意識したこと

  • 外部の Vacuumを呼び込む部分のロジックはclassにして、その中でそれぞれの外部に依存したメソッドを呼び出すようにする

これによって、テストではmockを使って、そのclassのインスタンスに対して応答する値を設定しやすくなった。

classはどのディレクトリに入れるべきか。
app/libs/amazon_product_api_caller.rb
のように、外部サービスに関するロジックだったので、 app/libs 以下に作成しました。

app/libs/amazon_product_api_caller.rb
class AmazonProductApiCaller
  def initialize
    @request = amazon_api_request
  end
  
  def search_items
    @request.search_items({
      search_index: 'All',
      market_place: 'www.amazon.co.jp',
      resources: [
        'ItemInfo.Title',
        'Offers.Summaries.LowestPrice'
      ],
      keywords: 'ねこまみれ'
    })&.to_h
  end

  private

  def amazon_api_request
    return Vacuum.new if Rails.env.test?

    Vacuum.new(marketplace: 'JP',
               access_key: 'xxxxxxxxxx',
               secret_key: 'yyyyyyy',
               partner_tag: 'hoge_fuga')
  end
end

lib/tasks/item.rake
namespace :item do
  desc 'amazon apiで取得した商品を登録する'
  task insert_from_amazon_search_item: :environment do
    # request作成
    amazon_product_api_caller = AmazonProductApiCaller.new

    # search_items を呼び出す
    response = amazon_product_api_caller.search_items

    response['SearchResult']['Items'].each do |item|
      new_price = item['Offers']['Summaries'].detect do |summary|
        summary['Condition']['Value'] == 'New'
      end

      # itemの作成
      Item.create name: item['ItemInfo']['Title']['DisplayValue'], price: new_price['LowestPrice']['Amount']
    end
  end
end

このような状態になったら、いよいよ、テストにもう一度向き合います。
参考にさせていただいたQiitaの記事 ありがとうございます。 :bow:
使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」

spec/lib/tasks/item_spec.rb

require 'rails_helper'
require 'rake'

RSpec.describe 'item:insert_from_amazon_search_item' do
  before(:all) do
    @rake = Rake::Application.new
    Rake.application = @rake
    Rake.application.rake_require 'tasks/item'
    Rake::Task.define_task(:environment)
  end
  

  before(:each) do
    @rake[task_name].reenable
    # 実際に返ってくるレスポンスと同じような値を用意する。
    response_hashed = {
      "SearchResult" => {
        "Items" => [{...}]}
    }

    # AmazonApiに関するモックを用意。(外部との難しい部分をうけおってくれるモックになる。頼むぞ!
    let(:amazon_api_double) { double('amazon api caller') }
    # Vacuum.new に対しては、こちらで用意したモックを返すようにする
    allow(Vacuum).to receive(:new).and_return(amazon_api_double)
    # こちらで用意したモック(Vacuumのフリをするモック)に対してsearch_itemsを呼べるようにする。
    allow(amazon_api_double).to receive(:search_items)
    # AmazonProductApiCaller のインスタンスに対してsearch_itemsが呼ばれたら、テストで用意した値が返るようにする。
    allow_any_instance_of(AmazonProductApiCaller).to receive(:search_items).and_return(response_hashed)

  end

  context 'when valid response returns,' do
    let(:task_name) { 'item:insert_from_amazon_search_item' }
    it 'creates new item.' do
      expect do
        @rake[task_name].invoke
      end.to change(Item, :count).by(1)
    end
  end
end

こうして無事テストを書くことができました。 :tada:

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?