この記事で書くこと
- 最初のコードからどのように変化したか。
- モックのテストが苦手な人間がどのようにテストを乗り越えたか
- double
- allow_any_instance_of
- allow
この記事で書かないこと
- 使用しているgemの詳しい説明
- Amazon Product Advertising API 5.0. に関する詳しいapi仕様
- rakeタスクのspecの良い(適した、or正しい)書き方
一番伝えたい事
- テストを書く事は、より良いコードを書く為の近道
テストしようと思ったコード
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_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の記事 ありがとうございます。
使える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
こうして無事テストを書くことができました。