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

## この記事で書くこと
- 最初のコードからどのように変化したか。
- モックのテストが苦手な人間がどのようにテストを乗り越えたか
  - 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://webservices.amazon.com/paapi5/documentation/quick-start/using-sdk.html)はない為、
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_h` は `Array#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）を使ったテストの書き方」](https://qiita.com/jnchito/items/640f17e124ab263a54dd)

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