テストコード
投稿日: 2025年07月26日
実務では必ず書くテストコード、、
私はオリアプ1つめでJESTを使用したテストコードを書きました。
その後ほぼ書く事なくきていましたが、4月から実務にガッツリ入り始めてからよく書くようになり、最近テスト駆動開発という名前を聞いたりテストについて質問されることもあったりでちょっと意識が上がってきたのでRailsの内容ですがテストについて書いてみようと思います。
簡単そうに見えてテストコード書くのには結構時間がかかります。
このロジックのテストをするにはどんなテストケースを考えたら良いか、どこをモックすべきか、ダミーデータはどうするのが適切かなど、システムの品質を担保していると言えるテストになっているか考えることは普通に大変で難しいことです。
品質の担保
コードが期待通りに動作するかを確認し、バグを早期に発見します。
安全な変更の保証:
コード修正時に既存機能への影響がないことを確認し、デグレを防ぎます。これにより、安心してリファクタリングや機能追加ができます。
仕様の明確化
テストコード自体が「コードが何をすべきか」を示す生きたドキュメントとなり、チームの理解を深めます。
開発効率の向上
バグ修正やデバッグにかかる時間を削減し、長期的には開発全体の速度を向上させます。
「リファクタしたつもりが挙動変えちゃった〜」って時にテストがあることで気づくと言うのもありますし、テスト駆動開発というのもあるらしく、正常系、異常系含めどう振る舞うべきか考えること自体が品質の向上につながりそうです。
前提としてテストはいつ誰が実行してもテスト結果は一緒になるべきです。
・境界値テストで現在時刻を使用
例えば、キャンペーン等である日時を境に出力結果を変えたいと言うケースがあったとします。
そのテストをする時に渡す日時が現在時刻だと実行するときによって変わってしまいますので、適切ではないです。
こういった同じコード・同じテストなのに、実行するたびに通ったり失敗したりするテストのことをFlakyなテストと言うそうです。
ある日時を境にちゃんと出力結果が変わるか見ないといけないので、必要なテストケースはその特定の日時の前後であることが必須ですよね、と言うことになります。
具体的には2025-08-01 00:00:00
を境に挙動が変わると言うことであれば、
2025-07-31 23:59:59
(境界の直前)
2025-08-01 00:00:00
(境界点そのもの)
2025-08-01 00:00:01
(境界の直後)
これらをテストすることでどこで分岐しているか正確にテストすることができます。
このように意味あるテストになるようにテストケースを考える必要があります。
こういうことまで詳しく教えてくれるボスはほんとに神だと思う
次には、実際のテストコードについてです。
FactoryBot
Rspec
Rswag
主にDB設計してエンドポイント作るとこの3つでテストコードを書きます。
Railsの経験ある方なら当たり前なんですかね?
テスト用のダミーデータ(テストオブジェクト)を簡単に作るツールです。
テーブルを作成した時に、そのテーブルのレコードをテストで使用する際に簡単にダミーデータを作成するために使用します。
Ruby(特にRails)で書くテストコードのフレームワークです。
controllerなど単体のロジックのテストはこれで書きます。
RSpecのAPIテストから自動でAPI仕様書(Swaggerドキュメント)を作るツールです。
リクエストとレスポンスといったAPIの挙動をテストします。
DB設計込みでエンドポイント作成すると、この3つのテストコードを必ず書きます。
足りてないままレビュー出すと「スキーマ(rswag)のテストも書いてくれ〜」と戻ってきますw
大変だなと感じるのは、テーブルのカラムの追加があった時に、modelとschemaの修正で終わりそうなところが、nil:falseとかだとテストにも影響することがあるので、ちょっとの修正でテストも修正しないといけんかったりがあるところです。。
一つずつ詳細みていきます。
正直存在しているコードを確認したりclaudeを使ったりしているとふんわりしかわからなかったので書きながらここで理解したいですw
FactoryBot.define do
factory :model_name do
...
end
end
model_nameモデル用のfactoryを作成します。
使用する時はテストコードの中で以下のように記述します。
create(:model_name)
関連モデルを定義します。
ファクトリー使用する際にこのファクトリーも生成されます。
association :model_hoge
association :model_fuga
アソシエーションで定義しているモデルのファクトリーないとテスト通らないので先に作っている必要があります
name { Faker::Name.name }
name_kana { 'テスト' }
phone_number { Faker::PhoneNumber.subscriber_number(length: 10) }
Fakerでランダムなダミーデータが作成されます。
直接文字列を指定して書くこともできます。
Fakerが持っているメソッドなどはドキュメント確認すると良いです!
deleted_at { nil }
is_default { false }
deleted_atは日付かnil(null)なのでnilをファクトリーのデータにはセット
is_defaultはbooleanなのでfalseをセットします。
テストケースに応じてファクトリーのデータの値を変えたい時があるので、特定の条件付きで生成する際に使用します。
# デフォルトがtrueのデータを使いたい時
trait :default do
is_default { true }
end
# 論理削除されたデータを使いたい時は時刻を入れる
trait :deleted do
deleted_at { Time.current }
end
使い方は、 以下のようにします。
create(:model_name, :default, :deleted)
RSpec は、Ruby(特に Rails)で最もよく使われるテストフレームワークらしいです。
「describe」「context」「it」などの構造はTypeScriptで使用するJESTと変わらないなと言う印象を受けました!!
RSpec.describe クラス名, type: :controller do
describe 'メソッドや処理の名前' do
context 'ある条件のとき' do
it '期待する結果になること' do
expect(処理).to eq(期待値)
end
end
end
end
テスト内で使用するデータを予め定義しておきます。
beforeの中の処理はitの処理が走った時点で実行され、letは呼び出されたタイミングで実行されます。(時間とか!の有無でズレたりするので注意です⚠️)
let(:hoge) { create(:hoge) } # 遅延実行(:hoegが使われるとき実行)
let!(:fuga) { create(:fuga) } # 即実行
before do
login(user)
end
モック・スタブは、本物のオブジェクトや処理を呼び出さずに、動作や戻り値を疑似的に定義することです。
スタブ(allow)
「このメソッドが呼ばれたらこう返す」という 挙動のすり替え
allow(SomeService).to receive(:call).and_return(result)
モック(expect)
「このメソッドが呼ばれたことを検証したい」という 期待の設定
expect(SomeService).to receive(:call).once
例えば、先ほどの時刻のモックを作成する場合
let(:fixed_time) { Time.zone.parse('2025-08-01 00:00:00') }
before do
allow(Time).to receive(:current).and_return(fixed_time)
end
fixed_timeと言う変数に2025-08-01 00:00:00を入れておいて、Timeのcurrentが呼ばれたらfixed_timeの値を返すと処理自体をテスト向けに変えています。
get :index
post :create, params: { data: hoge_params }
patch :update, params: { id: hoge.id, data: update_params }
delete :destroy, params: { id: hoge.id }
expect(response).to have_http_status(:ok) # 200
expect(response).to have_http_status(:created) # 201
expect(response).to have_http_status(:unprocessable_entity) # 422
expect(response).to have_http_status(:not_found) # 404
parsed = response.parsed_body
expect(parsed).to be_a(Hash)
expect(parsed['data']).to be_an(Array)
expect(parsed['data']['id']).to eq(hoge.id)
expect(parsed['errors']).to include("Name can't be blank")
expect {
post :create, params: { data: valid_params }
}.to change(Brand, :count).by(1)
expect {
delete :destroy, params: { id: hoge.id }
}.to change(Hoge, :count).by(-1)
色々ありますが、こんな感じですねぇ
Rswag は、RSpec で書いた API テストから Swagger 形式の API 仕様書を自動生成できるツールです。
Rails で API を開発していると、仕様書の整備・更新が結構大変だと思うのですが、Rswag を導入しておくとコードとドキュメントのズレが防げるので便利だと思います。
RSpec.describe 'api/v1/rswag_examples', type: :request do
before { host! 'localhost:0000' } # リクエスト先ホストを明示
path '/rswag_hoge' do # 対象となるAPIのエンドポイント
get('hage') do # HTTPメソッドと説明(ここでは GET)
tags 'RswagHoge' # Swagger上のグループ名(タグ)
produces 'application/json' # レスポンスのContent-Typeを定義
response(200, 'successful') do # ステータスごとのレスポンス定義
run_test! # 実行とドキュメント生成を同時に行う
end
end
end
end
before { host! 'localhost:0000’ }
リクエスト先ホストを明示します。
rswagで記述したパスをこのホストで解決するためです。
path '/rswag_hoge' do
...
end
テスト対象のエンドポイントパスを定義します。
このスコープ内にこのエンドポイントの処理を書きます。
get('hello world') do
...
end
HTTPメソッド(この例ではGET)と、その処理の説明を記述します。
tags 'RswagHoge'
APIドキュメントで分類するためのタグです。
Swagger UIでのグルーピングに使われます。
produces 'application/json'
レスポンスが返すメディアタイプ(Content-Type)を指定します。
response(200, 'successful') do
...
end
HTTPステータスごとのテストとドキュメントの定義です。
テストコードと仕様の両方をこの中で一緒に書けます。
エラー場合もここに書いていく流れになります。
ShiftB生の参考にはあんまりならない内容かなと思いますが、完全実務ベースな私個人のアウトプットとして書かせていただきました。
実装だけならそんなに時間かからないのに、テストコードあるから時間かかるって思うときもありますし、こういうのはAIに任せていいのではと思いつつわかってない人がA Iに任せてはいけないというのは最近特に感じているのでしっかり理解し、自分の頭で考えてすらすら書けるようになりたいです!!!(そうなってからAIに外注)