RSpecもスタブも知らなかった私が、RSpec実装時に外部リクエスト部分をスタブ化した話

はじめに

こんにちは。crispyでエンジニアインターンをしている雨宮佳音です。crispyが運営するHEIMには、商品の価格情報を取得するため、外部サイトにアクセスを行う処理があります。RSpecもスタブも知らなかった私が、RSpec実装において外部リクエスト処理をスタブ化したので、そこで学んだことをまとめていきます。

背景

HEIMの商品価格周りのコード改修後に、テストでエラーが発生しました。原因を調査してみると、外部サイトの影響でテストが失敗しているようでした。

crispyに入るまでは小規模な個人開発の経験しかなかったため、当時の私はRSpecやスタブどころか、テストで何をするのかもよくわかっていませんでした。そこで、RSpecの記事やHEIMの既存のコードからテストについて勉強し、外部サイトの状況に依存しないテストに変更するため、スタブ化を行いました。

スタブ化とは

テストでエラーが発生したとき、初めはなぜそれがエラーになっているのかがわかりませんでした。前に同じコードでテストを走らせたときには成功したのに、どうして失敗してしまうことがあるのか疑問に思いました。社員の方に質問したところ、該当のテストコードがスタブ化されていないことにより、テストが失敗することがあると教えていただきました。そこで初めて聞いた「スタブ」という言葉について、様々な記事を読んで理解を深めました。

スタブとは、テスト内で使用される「あらかじめ中身が決められたオブジェクト」のことです。そしてスタブ化とは、テスト実行時に、他のメソッド・外部サイトに飛んでオブジェクトを取ってくるのではなく、事前にオブジェクトの中身を宣言することです。これを行う理由は大きく3つあります。

機能実装前にテストを実装するため

大規模なシステム開発を行ったとき、一部の完成済みプログラムの動作検証を行うためです。完成済みプログラムが、未完成プログラムのオブジェクトを必要とする場合を想定します。スタブ化を行わなければ、未完成プログラムを仕上げてからでないと、完成済みプログラムのテストを実行できません。ここでスタブを用いれば、オブジェクトを代用してテストを実行できます。

テストを別メソッドの動作に依存させないため

テストを他のプログラムの不確かさに依存させないためです。例えば、テスト対象(calculateメソッド)とは別のメソッド(numberメソッド)からオブジェクトを取得して、処理を行うプログラムを想定します。

def number
  return 5
end

def calculate(number)
  result = number + 10
end

numberメソッドのコードが適切でない場合、calculateメソッドのテストに影響を与えてしまいます。テストをnumberメソッドの結果に依存させないようスタブ化することで、calculateメソッドだけの確かさを検証できます。

テストを外部サイトの状況に依存させないため

テストを外部サイトの状況に依存させないためです。外部サイトからデータを取得して、処理を行うプログラムを想定します。例えば、このプログラムのテスト実行時に、外部サイトがサーバー落ちしていた場合、プログラム自体に問題がなくてもテストが成功しません。外部サイトから取得するデータをあらかじめ宣言しておけば、対象のプログラム自体を検証できます。 また、外部サイトにアクセスすると発生する、通信の時間を短縮することもできます。

スタブとモック

テストについて勉強していくうちに、「モック」という言葉も出てきました。似たような働きをするため、これらの違いを理解するのにはかなり時間を費やしました。スタブとモックは、どちらもテストを円滑に進めるのに使う道具ですが、役割が異なります。

スタブは、テスト対象に対して渡す、あらかじめ中身が決められたオブジェクトのことです。スタブ化を行うことで、ある処理がそのオブジェクトを受け取ったとき、期待する結果を返すかどうかを確認できます。

一方モックは、テスト対象からデータを受け取り、それを検証するオブジェクトのことです。モック化を行うことで、ある処理が期待する引数・呼び出し回数で呼ばれているかを確認できます。

今回は、テスト対象が商品の価格情報を受け取ったとき、期待する結果を返すかどうかを確認したかったため、モックではなくスタブを使うことにしました。

実際にスタブ化してみる

スタブについて理解したところで、先ほどのコードを例に、スタブ化をしてみます。

def calculate(number)
  result = number + 10
end
it 'calculate correctly' do
  allow(number).to receive(5)
  expect(result).to eq (15)
end

calculateメソッドが正しく実行されるかどうかのテストです。calculateメソッド内ではnumberメソッドの戻り値が使用されています。ここでは、numberメソッドの戻り値の不確かさに依存しないようにするため、「numberメソッドの戻り値は5だよ」と事前に宣言をしています。

Typhoeus使用時のスタブ化

次に、実際のHTTPリクエストを例に、スタブ化を行います。ここではHEIMで使用しているTyphoeusを使い、商品情報を取得するプログラムのテストを書いています。TyphoeusはRSpec独自のスタブを使う必要がなく、簡単にスタブ化ができます。

def get_product_name(url)
  product_information = Typhoeus.get(url)
  # ここにproduct_informationのデータから商品名(product_name)を取得する処理を書く
  return product_name
end

product_informationのデータから商品名(product_name)を取得する処理は、ここでは割愛します。 続いてテストコードです。

it 'get_product_name correctly' do
  response = Typhoeus::Response.new
  Typhoeus.stub('www.example.com').and_return(response)
  expect(get_product_name).to eq ('#URLに該当する商品名')
end

テストにおいて

Typhoeus.get('www.example.com')

が実行されたときには、responseを返すことを事前に宣言し、スタブ化しています。こうすることで、テスト実行時にTyphoeus.getがうまくいかなくても、それ以下の、商品名を取得するコードの確かさを検証することができます。

まとめ

HEIMのテスト実装では、Typhoeusを使った方法でスタブ化を行い、失敗していたテストが成功するようになりました。新たなスキルを身につけてcrispyに貢献できたことで、インターン生としての成長を実感しています。ここで学んだことを活かせるよう、現在別のテスト実装のタスクも任せていただいています!

crispyでは、HEIM、3rdmallの開発を一緒に盛り上げてくれるエンジニアを募集しています。インターン生でもHEIM・3rdmallを作り上げる重要なタスクを任せていただけるので、たくさんのスキルを身につけて、成長できます!興味がある方は、ぜひWantedlyのページから応募してください!