はじめに
こんにちは。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のページから応募してください!