はじめに
SimpleCovを導入してカバレッジを計測したところ、32.9% という現実を突きつけられました。
SimpleCovって何という方はこちら
SaveDataにはゲーム検索機能があり、以下の3つの外部APIと連携しています。
- Twitch(IGDBのアクセストークン取得)
- IGDB API(ゲーム検索)
- DeepL API(日本語クエリを英語に翻訳)
テストを実行するたびに本物のAPIを叩いていると…
- APIの制限にすぐ引っかかる
- ネットが不安定だとテストが落ちる
- 実行速度が遅くなる
そこで WebMock を導入して外部APIへのリクエストをモック化しました。
☆この記事でわかること☆
- WebMockとは何か・なぜ使うのか
- 複数の外部APIを同時にモック化する方法
-
.to_raiseでエラーハンドリングのテストを書く方法
【ちょっと宣伝】こんなアプリを作っています。
ゲームx思い出を記録できるアプリ SaveData
WebMockとは?
テスト中に外部APIへのHTTPリクエストをブロックして、代わりに偽のレスポンスを返してくれるgemです。
Mockっていうのはモックアップの略で商品化前の試作品や模型のこと、
エンジニア業界でいう
ダミーオブジェクト: テストや設計の段階で、
本物の機能を模倣するために用意される仮のシステムやコンポーネントを指します。
通常のテスト WebMockあり
RSpec → IGDB API(本物) RSpec → WebMock(偽物)→ 即返答
遅い・API制限あり 速い・制限なし
本物のAPIが起動していなくてもテストが通るのが最大のメリットです。
導入
# Gemfile
group :test do
gem 'webmock'
end
# spec/rails_helper.rb
require 'webmock/rspec'
# 外部APIへの実通信をブロック(localhostは許可)
WebMock.disable_net_connect!(allow_localhost: true)
IgdbServiceの中身(抜粋)
class IgdbService
# ゲームを検索する(日本語・英語を自動判定)
def self.search(query)
token = get_token # Twitchでトークン取得
if contains_japanese?(query)
results = search_by_alternative_names(query, token)
english_query = translate_to_english(query) # DeepLで翻訳
results += search_games_directly(english_query, token) if english_query
results.uniq { |g| g["id"] }
else
search_games_directly(query, token)
end
end
private_class_method def self.get_token
# Twitchからアクセストークンを取得(1時間キャッシュ)
Rails.cache.fetch("igdb_token", expires_in: 1.hour) do
res = Net::HTTP.post_form(URI("https://id.twitch.tv/oauth2/token"), ...)
JSON.parse(res.body)["access_token"]
end
end
private_class_method def self.translate_to_english(query)
# DeepL APIで翻訳(失敗時はnilを返す)
...
rescue => e
nil
end
end
テストコード一部抜粋
require 'rails_helper'
RSpec.describe IgdbService do
before do
# Twitchのトークン取得をモック化(全テスト共通)
# IgdbService.searchを呼ぶと必ずここを通るのでモックが必須
stub_request(:post, "https://id.twitch.tv/oauth2/token")
.to_return(
status: 200,
body: { access_token: "fake_token", expires_in: 3600 }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
# 前のテストのトークンキャッシュが残らないようにクリア
Rails.cache.clear
end
describe ".search" do
context "英語で検索したとき" do
before do
stub_request(:post, "https://api.igdb.com/v4/games")
.to_return(
status: 200,
body: [
{
"id" => 1,
"name" => "Final Fantasy VII",
"cover" => { "image_id" => "abc123" },
"platforms" => [ { "name" => "PlayStation" } ],
"genres" => [ { "name" => "RPG" } ]
}
].to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
it "ゲームが返ってくること" do
results = IgdbService.search("Final Fantasy VII")
expect(results).not_to be_empty
end
it "nameが含まれること" do
results = IgdbService.search("Final Fantasy VII")
expect(results.first["name"]).to eq("Final Fantasy VII")
end
it "cover_urlが付加されること" do
results = IgdbService.search("Final Fantasy VII")
expect(results.first["cover_url"]).to include("abc123")
end
end
context "日本語で検索したとき" do
before do
# DeepL APIをモック化(日本語→英語翻訳)
stub_request(:post, "https://api-free.deepl.com/v2/translate")
.to_return(
status: 200,
body: {
translations: [ { text: "Final Fantasy VII" } ]
}.to_json,
headers: { 'Content-Type' => 'application/json' }
)
# alternative_names検索をモック化(日本語タイトル検索)
stub_request(:post, "https://api.igdb.com/v4/alternative_names")
.to_return(
status: 200,
body: [
{
"name" => "ファイナルファンタジーVII",
"game" => {
"id" => 1,
"name" => "Final Fantasy VII",
"cover" => { "image_id" => "abc123" }
}
}
].to_json,
headers: { 'Content-Type' => 'application/json' }
)
# 英語翻訳後のIGDB検索もモック化
stub_request(:post, "https://api.igdb.com/v4/games")
.to_return(
status: 200,
body: [].to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
ポイントまとめ
① Twitchのトークン取得は全テスト共通でモック化
IGDBを使うとき必ずTwitchのトークン取得を通るため、一番外の before に書きます。
また Rails.cache.clear でテスト間のキャッシュ汚染を防ぎます。
② 日本語検索は3つ同時にモック化が必要
日本語で検索すると内部でDeepL・alternative_names・gamesの3つのAPIを叩くため、全部モック化しないとエラーになります。
③ .to_raise でエラーハンドリングもテストできる
stub_request(:post, "URL").to_raise(StandardError.new("connection failed"))
rescue ブロックの中まで通すことができます。カバレッジの赤い行を消すのに非常に有効です。
rescueっていうのは
rescue ブロックとは例外(エラー)が発生したときの安全装置。
DeepLが落ちていてもアプリが止まらないよう nil を返しています。
.to_raise でその安全装置が本当に動くかを確認できます。
まとめ
| やったこと | 結果 |
|---|---|
| WebMock導入 | 外部APIなしでテストが通るようになった |
| Twitch・IGDB・DeepLをモック化 | IGDBサービスのカバレッジが大幅アップ |
| エラーケースのテスト |
.to_raise でrescueブロックもカバー |
| カバレッジ | 32.9% → 93% |
やって良かったこと🌟
- APIの制限を気にせずテストを何度でも実行できるようになった
- エラーハンドリングが本当に機能しているか検証できた
最後まで読んでいただきありがとうございました🙏
こんなアプリを作っています。よければ触ってみてください!
ゲーム × 思い出を記録できるアプリ SaveData