💭
【RSpec入門】スタブって何?外部API連携のテストを安全・高速にする方法
【RSpec入門】スタブって何?外部API連携のテストを安全・高速にする方法
はじめに
Rubyでアプリケーション開発をしていると、外部のAPIやサービスと連携する機会が多くありますよね。
でも、これらの外部サービスを使う処理をテストするときに、こんな困った経験はありませんか?
- テスト中にAPIサーバーが落ちて、テストが失敗してしまった 😭
- 外部APIの呼び出しでテストの実行時間が長くなってしまった ⏰
- テスト環境でインターネット接続ができない 🚫
そんな問題を解決してくれるのが、RSpecのスタブという機能です!
スタブとは?簡単に言うと...
スタブ = 外部サービスの「影武者」
外部のAPIやサービスに実際にアクセスする代わりに、「このAPIを呼んだら、こんなデータが返ってくるよ」という偽の応答を用意してくれる仕組みです。
例えで理解するスタブ
映画撮影で考えてみましょう。
- 本物の俳優(外部API):スケジュールが合わない、ギャラが高い
- スタントマン(スタブ):いつでも使える、安い、決まった演技をしてくれる
テストでも同じです!
実際のコードで見てみよう
1. 外部APIを呼ぶクラスを作成
まず、写真データを取得するサービスクラスを作ってみます。
# api_service.rb
require 'net/http'
require 'json'
class ApiService
def fetch_photos
# 実際の外部APIにアクセス
uri = URI('https://jsonplaceholder.typicode.com/photos')
response = Net::HTTP.get(uri)
JSON.parse(response)
end
end
このクラスは、JSONPlaceholderという無料のAPIから写真データを取得します。
2. スタブなしのテスト(問題あり)
# 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にアクセスするので遅い
- 🚫 ネットワークがないと実行できない
- 💥 APIサーバーが落ちるとテストが失敗する
3. スタブありのテスト(解決!)
# 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: "テスト用写真",
url: "https://example.com/photo1.jpg"
},
{
id: 2,
title: "もう一つの写真",
url: "https://example.com/photo2.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')
expect(photos.length).to eq(2)
end
end
end
スタブの書き方を分解して理解しよう
allow(Net::HTTP).to receive(:get).and_return("返したい値")
これを日本語にすると:
-
allow(Net::HTTP)
→ "Net::HTTPクラスに対して" -
.to receive(:get)
→ "getメソッドが呼ばれたら" -
.and_return(...)
→ "この値を返してね"
より具体的な例
# 文字列を返す場合
allow(Net::HTTP).to receive(:get).and_return('{"message": "success"}')
# 配列を返す場合
allow(Array).to receive(:new).and_return([1, 2, 3])
# nilを返す場合
allow(SomeClass).to receive(:some_method).and_return(nil)
エラーケースもテストしてみよう
外部APIでは、ネットワークエラーやサーバーエラーも発生します。
スタブを使えば、これらのエラー状況も簡単にテストできます!
RSpec.describe ApiService do
describe '#fetch_photos' do
context 'APIが正常に動作する場合' do
before do
allow(Net::HTTP).to receive(:get).and_return(
[{ id: 1, title: "写真", url: "https://example.com/1.jpg" }].to_json
)
end
it '写真データが取得できる' do
service = ApiService.new
photos = service.fetch_photos
expect(photos).to be_an(Array)
end
end
context 'ネットワークエラーが発生する場合' do
before do
# エラーを発生させるスタブ
allow(Net::HTTP).to receive(:get).and_raise(SocketError.new("接続できません"))
end
it 'SocketErrorが発生する' do
service = ApiService.new
expect { service.fetch_photos }.to raise_error(SocketError)
end
end
end
end
スタブのメリット・デメリット
👍 メリット
- 高速: 実際のAPI呼び出しがないので爆速
- 安定: 外部サービスの状態に左右されない
- 柔軟: エラーケースも自由にテストできる
- コスト削減: API使用量を気にしなくて良い
👎 デメリット
- 現実との乖離: 実際のAPIとは異なる可能性
- 仕様変更に気づきにくい: APIの仕様が変わっても気づかない場合がある
スタブを使うべき場面
🟢 スタブを使うべき
- 外部API呼び出し
- データベースアクセス
- ファイル読み書き
- 時間に依存する処理(現在時刻など)
- 実行に時間がかかる処理
🔴 スタブを使わない方が良い
- 自分のアプリケーション内のロジック
- 単純な計算処理
- 文字列操作など
実践的なコツ
1. 必要最小限のデータを用意する
# ❌ 実際のAPIと同じ巨大なデータ
allow(Net::HTTP).to receive(:get).and_return(huge_realistic_data)
# ✅ テストに必要な最小限のデータ
allow(Net::HTTP).to receive(:get).and_return(
[{ id: 1, title: "test", url: "https://example.com/1.jpg" }].to_json
)
2. letを使ってテストデータを整理する
RSpec.describe ApiService do
let(:mock_photos) do
[
{ id: 1, title: "写真1", url: "https://example.com/1.jpg" },
{ id: 2, title: "写真2", url: "https://example.com/2.jpg" }
]
end
before do
allow(Net::HTTP).to receive(:get).and_return(mock_photos.to_json)
end
# テストコード...
end
3. スタブが正しく呼ばれているかチェック
it '正しいURLでAPIを呼び出す' do
service = ApiService.new
service.fetch_photos
# スタブが期待通りに呼ばれたかチェック
expect(Net::HTTP).to have_received(:get).with(URI('https://jsonplaceholder.typicode.com/photos'))
end
まとめ
スタブは、外部サービスとの連携をテストする際の強力な味方です!
覚えておきたいポイント:
- スタブ = 外部サービスの「影武者」
-
allow(...).to receive(...).and_return(...)
が基本の書き方 - テストが高速・安定・柔軟になる
- ただし、実際のサービスとの統合テストも別途必要
最初は慣れないかもしれませんが、使い始めるとその便利さに驚くはずです。
ぜひ実際のプロジェクトでも活用してみてください!
おまけ:よく使うスタブのパターン
# 時刻を固定する
allow(Time).to receive(:now).and_return(Time.parse('2023-01-01 12:00:00'))
# ランダムな値を固定する
allow(SecureRandom).to receive(:uuid).and_return('fixed-uuid-for-test')
# ファイル読み込みをスタブ化
allow(File).to receive(:read).and_return('テスト用ファイル内容')
Happy Testing! 🎉
Discussion