💭

【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

まとめ

スタブは、外部サービスとの連携をテストする際の強力な味方です!

覚えておきたいポイント

  1. スタブ = 外部サービスの「影武者」
  2. allow(...).to receive(...).and_return(...) が基本の書き方
  3. テストが高速・安定・柔軟になる
  4. ただし、実際のサービスとの統合テストも別途必要

最初は慣れないかもしれませんが、使い始めるとその便利さに驚くはずです。
ぜひ実際のプロジェクトでも活用してみてください!

おまけ:よく使うスタブのパターン

# 時刻を固定する
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