allowスタブで外部処理を置き換えてテストしよう

学習の目標

本章では、以下の内容を学習します。

  • スタブの基本概念と重要性を理解する
  • 外部サービスに依存するコードを安定してテストする方法を習得する
  • RSpecのallowメソッドを使ったスタブの実装方法を学ぶ
  • 正常系と異常系の両方をテストする手法を身につける

はじめに

本章では、RSpecにおける重要な機能である「スタブ」について学習します。これはテストを書く上でとても役立つテクニックです。

Rubyでアプリケーションを開発していると、外部のWebサイトやAPIとの連携が必要になることがよくあります。例えば、天気予報のデータを取得したり、決済情報を処理したりする場合などです。

しかし、これらの外部サービスを使用する処理をテストする際には、いくつかの課題が発生します。

  • 外部サービスが一時的に停止していると、テストが失敗してしまう
  • ネットワーク通信を行うので、テストの実行に時間がかかってしまう
  • テスト環境ではネットワーク接続が制限されていて、外部にアクセスできないことも多い

このような問題を解決するために、スタブという仕組みを活用します。スタブは、外部サービスの振る舞いを模倣し、決められた値を返すための機能です。つまり、実際の外部サービスとやり取りする代わりに、「この場合はこういう結果が返ってくるはずだよ」とあらかじめ設定しておくことができます。

外部APIを呼び出すコードの実装

具体的な例を通じて、スタブの使い方を学んでいきましょう。まずは、外部APIから写真データを取得する基本的なクラスを実装します。

写真共有サービスのAPIから写真情報を取得する簡単なクラスを作ってみましょう。api_service.rbというファイルを作成し、以下のコードを記述してください。

require 'net/http'
require 'json'
class ApiService
def fetch_photos
uri = URI('https://jsonplaceholder.typicode.com/photos')
response = Net::HTTP.get(uri)
JSON.parse(response)
end
end

このコードは何をしているのでしょうか?1行ずつ見ていきましょう。

require 'net/http'は、HTTPリクエストを行うための標準ライブラリを読み込みます。Webサイトやサービスとやり取りするために必要なものです。

次にrequire 'json'は、JSONデータを扱うための標準ライブラリを読み込みます。WebサービスはよくJSONという形式でデータをやり取りします。

そしてfetch_photosメソッドは、外部APIにアクセスして写真データを取得します。

fetch_photosメソッドの中では、まずURIクラスを使ってAPIのURLを指定します。次に、Net::HTTP.getメソッドを使ってそのURLにGETリクエストを送信します。このメソッドは、指定したURLからデータを取得し、その結果を文字列として返します。

まず、JSONPlaceholder(テスト用の無料API)のURLを指定します。次に、HTTPリクエストを送信してデータを取得します。最後に、取得したJSON文字列をRubyのハッシュや配列に変換します。

これで基本的なAPIサービスクラスができました。実際のアプリケーションでは、このようなコードを使って外部サービスからデータを取得することがよくあります。

単純なテストの課題

では、このApiServiceクラスの動作を確認するテストを実装してみましょう。まずは、スタブを使用しない基本的なテストコードから作ってみます。

spec/api_service_spec.rbというファイルを作成し、以下のコードを記述してください。

require_relative '../api_service'
RSpec.describe ApiService do
describe '#fetch_photos' do
it '写真データを取得できる' do
service = ApiService.new
photos = service.fetch_photos
expect(photos).to be_an(Array)
expect(photos.first).to include('id', 'title', 'url')
end
end
end

一見シンプルで良さそうなテストですが、このテストコードには実践的な課題があります。

まず、テストを実行するたびに実際のAPIにアクセスするため、ネットワーク通信の分だけテストの実行時間が長くなります。テストはたくさん書くものなので、1つ1つが遅いと全体の実行時間が大幅に増えてしまいます。

また、インターネット接続がない環境や、接続が不安定な環境では、ネットワーク依存による不安定さからテストが失敗してしまいます。さらに、テスト対象のAPIサーバーがメンテナンス中だったり、一時的に障害が発生していたりすると、テストが失敗します。

加えて、会社やプロジェクトによっては、セキュリティ上の理由からテスト環境でのインターネットアクセスが制限されていることもあり、環境依存の問題が発生する可能性があります。

こういった問題があると、テストが安定しないので、エラーの原因が自分のコードなのか、外部要因なのかを判断するのが難しくなります。安定したテストは、開発者の信頼と安心感につながるものです。

スタブを活用した解決策

上記の課題を解決するために、スタブを使用してテストコードを改善しましょう。スタブを使うと、実際のAPIアクセスを模擬的な処理に置き換えることができます。

spec/api_service_spec.rbを以下のように修正してください:

require_relative '../api_service'
RSpec.describe ApiService do
describe '#fetch_photos' do
before do
# Net::HTTPのgetメソッドをスタブ化
allow(Net::HTTP).to receive(:get).and_return([
{
id: 1,
title: "写真1",
url: "https://example.com/photo1.jpg"
}
].to_json)
end
it '写真データを取得できる' do
service = ApiService.new
photos = service.fetch_photos
expect(photos).to be_an(Array)
expect(photos.first).to include('id', 'title', 'url')
end
end
end

スタブの実装ポイント

このコードでは、allowメソッドを使ってNet::HTTP.getメソッドをスタブ化しています。これにより、実際のHTTPリクエストは発生せず、代わりに指定した値が返されるようになります。

allow(Net::HTTP).to receive(:get)は、HTTP通信をスタブ化します。「Net::HTTPクラスのgetメソッドが呼ばれたら...」という指示です。

and_return(...)では、スタブが返す値を指定しています。この場合、写真データのJSON文字列を返すようにしています。

テストに必要な最小限のデータ構造を定義しているのもポイントです。実際のAPIが返す全データをコピーする必要はなく、テストに必要な項目だけ含めれば十分です。

スタブ活用のメリット

スタブを使うことで、いくつかの重要なメリットが得られます。

実際のAPIアクセスが不要になるため、テストの実行速度が向上します。ネットワーク通信の待ち時間がなくなるので、テスト全体の実行時間が短縮されます。

さらに、ネットワーク環境やサーバーの状態に左右されない、安定したテストが可能になります。毎回同じ条件でテストができるので、結果も一貫性があります。

また、インターネット接続がない環境や、接続が制限された環境でもどこでもテストを実行できるようになります。これにより、開発環境を選ばずにテストが可能になります。

通常では再現が難しいエラー状況なども、スタブを使って簡単に再現できるため、エッジケースのテストが容易になります。外部APIからの特殊なレスポンスパターンもシミュレートできます。

エラーケースのテスト実装

スタブのもう一つの大きなメリットは、エラーケースのテストが容易になることです。実際のAPIでエラーを発生させるのは難しいですが、スタブを使えば簡単にエラー状況を再現できます。

以下のようにコードを拡張して、正常系とエラー系の両方をテストしてみましょう。

RSpec.describe ApiService do
describe '#fetch_photos' do
context '正常系' do
let(:photos_response) do
[
{
id: 1,
title: "写真1",
url: "https://example.com/photo1.jpg"
}
].to_json
end
before do
allow(Net::HTTP).to receive(:get).and_return(photos_response)
end
it '写真データを取得できる' do
service = ApiService.new
photos = service.fetch_photos
expect(photos).to be_an(Array)
expect(photos.first).to include('id', 'title', 'url')
end
end
context 'エラー系' do
before do
allow(Net::HTTP).to receive(:get).and_raise(SocketError.new("Failed to open TCP connection"))
end
it 'ネットワークエラー時には例外が発生する' do
service = ApiService.new
expect { service.fetch_photos }.to raise_error(SocketError)
end
end
end
end

このコードでは、contextブロックを使って正常系とエラー系のテストを分けています。そして、エラー系のテストではand_raiseメソッドを使って例外を発生させるようにスタブ化しています。

これにより、実際にネットワークが切断されている状況をシミュレートし、アプリケーションがそのような状況でどのように振る舞うかをテストできます。現実には再現が難しいエッジケースもテストできるのは、スタブの重要な利点です。

実用的なエラーハンドリングの実装

実際のアプリケーションでは、エラーが発生したときにそのままクラッシュするのではなく、適切にエラーを処理することが重要です。では、ApiServiceクラスにエラーハンドリングを追加してみましょう。

class ApiService
def fetch_photos
uri = URI('https://jsonplaceholder.typicode.com/photos')
response = Net::HTTP.get(uri)
JSON.parse(response)
rescue SocketError => e
# ネットワークエラーの場合は空配列を返す
puts "ネットワークエラーが発生しました: #{e.message}"
[]
end
end

そして、これに対応するテストも追加しましょう。

context 'エラー系' do
before do
allow(Net::HTTP).to receive(:get).and_raise(SocketError.new("Failed to open TCP connection"))
end
it 'ネットワークエラー時には空配列を返す' do
service = ApiService.new
photos = service.fetch_photos
expect(photos).to eq([])
end
end

このように、スタブを使うことで様々なシチュエーションを簡単にテストできます。適切なエラーハンドリングを実装することで、アプリケーションの堅牢性が向上します。

スタブの使いどころ

では、スタブはどのような場面で使うべきでしょうか?以下のようなケースが考えられます。

  • 外部APIやサービスとの通信が必要な場合
  • データベースアクセスが必要な場合
  • ファイル操作が必要な場合
  • 時間のかかる処理が必要な場合

どれも、実際の処理を行うとテストが遅くなったり、環境依存の問題が発生したりする可能性があります。スタブを使うことで、これらの問題を回避し、安定したテストを実現できます。

特に、大規模なプロジェクトやチーム開発では、スタブを使うことでテストの信頼性が向上し、開発効率が大幅に改善されます。外部サービスに依存しないテストは、リファクタリングや新機能追加を行う際にも安心感をもたらします。

まとめ

本章では、RSpecのスタブ機能について学習しました。スタブは外部依存のあるコードをテストする際の重要な技術です。

今回は、以下のポイントを学びました。

  • スタブを使うことで、外部APIやサービスに依存せずにテストを実行できる
  • スタブを使うことで、テストの実行速度が向上し、安定したテストが可能になる
  • スタブを使うことで、エラーケースのテストが容易になる
  • スタブを使うことで、実際の環境に依存しないテストが可能になる

少し難しかったかもしれませんが、スタブを使うことでテストを書きやすく、より信頼性の高いテストを実現できますので、ぜひ活用してみてください。

このセクションは有料サブスクリプションへの登録、またはログインが必要です。完全なコンテンツにアクセスするには、料金ページ(/pricing)をご覧ください。購入済みの場合は、ログインしてください。

Basicプランでより詳しく学習

この先のコンテンツを読むにはBasicプラン以上が必要です。より詳細な解説、実践的なサンプルコード、演習問題にアクセスして学習を深めましょう。

作成者:とまだ
Previous
beforeでテスト実行前の処理を共通化しよう