目的
RSpecを使った基本的な進め方と、修正時に意識する観点を整理したいと思いましたので書きました。
開発における単体テストは、小さな単位のコードが期待どおりに動くかを確認するためのテストです。
たとえば、メソッド1つ、クラス1つ、モデル1つなど、比較的小さい単位を対象にします。
単体テストを書く主な目的は、次のとおりです。
- 仕様どおりに動くことを確認する
- 修正時のデグレード(既存機能の壊れ)を防ぐ
- リファクタリングを安全に進める
- コードの使い方や期待する振る舞いを明確にする
単体テストがない場合、コード修正のたびに手動確認が増えます。
その結果、次のような問題が起きやすくなります。
- 修正箇所とは関係ない部分を壊してしまう
- 動作確認に時間がかかる
- 何が正しい状態かが人によってぶれる
- 後からコードを読み返したときに仕様がわかりにくい
逆に単体テストがあると、修正後にテストを実行することで、少なくとも重要な処理が壊れていないかを機械的に確認できます。
Rubyで単体テストを書く方法
Rubyで単体テストを書く方法としては、大きく次の2つがあります。
Minitest
Ruby / Rails で標準的に使えるテストフレームワークです。
特に Rails では標準の仕組みとして使われます。
RSpec
Rubyで非常に有名なテストフレームワークです。
describe や it を使って、コードの振る舞いを読みやすく表現できます。
今回は、記事としてわかりやすく、現場でもよく見かける RSpecベース
で説明します。
RubyでRSpecを使う設定方法
ここでは、シンプルな Ruby プロジェクトで RSpec を使う基本手順を書きます。
RSpec の導入手順
# 1. Gemfile に以下を追加
group :development, :test do
gem 'rspec'
end
# 2. インストール
bundle install
# 3. 初期設定ファイルを作成
bundle exec rspec --init
これで通常、以下のようなファイルが作成されます。
.rspecspec/spec_helper.rb
簡単なコード例
たとえば、税込価格を計算するクラスを作るとします。
アプリ側のコード
# price_calculator.rb
class PriceCalculator
def self.with_tax(price)
(price * 1.1).floor
end
end
テストコード
# spec/price_calculator_spec.rb
require_relative '../price_calculator'
RSpec.describe PriceCalculator do
describe '.with_tax' do
it '100円に消費税を加算して110円を返す' do
expect(PriceCalculator.with_tax(100)).to eq(110)
end
it '333円に消費税を加算して366円を返す' do
expect(PriceCalculator.with_tax(333)).to eq(366)
end
end
end
実行方法
bundle exec rspec
RSpec では、次のような形でテストを書きます。
-
RSpec.describeでテスト対象を書く -
describeで機能や条件をまとめる -
itで期待する結果を1件ずつ書く -
expectで実際の結果と期待値を比較する
Railsで使う場合の設定
Rails では標準でテスト機能がありますが、RSpec を使いたい場合は rspec-rails
を追加します。
Gemfile
group :development, :test do
gem 'rspec-rails'
end
インストール
bundle install
bundle exec rails generate rspec:install
これにより、Rails向けの spec 環境が作成されます。
Rails向けの簡単な例
たとえば、モデルにバリデーションがある場合です。
モデル
# app/models/user.rb
class User < ApplicationRecord
validates :name, presence: true
end
spec
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
it 'nameがあれば有効である' do
user = User.new(name: 'Takamatsu')
expect(user.valid?).to be true
end
it 'nameがなければ無効である' do
user = User.new(name: nil)
expect(user.valid?).to be false
end
end
このように、モデル単位で「どういう条件なら有効か」「どういう条件なら無効か」を確認するのが、単体テストの基本です。
specファイルはどう作るのか
RSpec では、基本的に テスト対象に対応する spec ファイル を作ります。
たとえば、次のような対応にするとわかりやすいです。
| テスト対象 | specファイル |
|---|---|
app/models/user.rb |
spec/models/user_spec.rb |
app/services/price_calculator.rb |
spec/services/price_calculator_spec.rb |
lib/discount_calculator.rb |
spec/lib/discount_calculator_spec.rb |
つまり、どのコードをテストしているかが見てすぐわかる場所に置くのが基本です。
specファイルの基本構造
RSpec の spec ファイルは、だいたい次の形になります。
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'バリデーション' do
it 'nameがあれば有効である' do
user = User.new(name: 'Taro')
expect(user.valid?).to be true
end
end
end
それぞれの意味は次のとおりです。
-
RSpec.describe
何をテストするのかを書く -
describe
どの機能・条件をまとめているかを書く -
it
期待する結果を1件ずつ書く -
expect
実際の結果が期待値と合っているか確認する
修正時に単体テストをどう作るか
実務では、新規開発だけでなく
既存コードの修正時に spec を追加・修正することが多いです。
このときの考え方はシンプルです。
1. まず「何を直したのか」を明確にする
たとえば、次のような内容です。
- 空文字を許可してしまっていたので禁止した
- 税込計算の端数処理が間違っていたので修正した
- 特定条件でエラーになる不具合を修正した
2. 修正内容に対応するテストを追加する
つまり、今回の修正で守りたい仕様をテストにするということです。
修正時の具体例
たとえば、割引計算のコードがあるとします。
修正前のコード
class DiscountCalculator
def self.price_after_discount(price, discount_rate)
price - (price * discount_rate / 100)
end
end
このコードは一見正しそうですが、discount_rate
に異常な値が来たときの想定がありません。
修正後のコード
class DiscountCalculator
def self.price_after_discount(price, discount_rate)
raise ArgumentError, 'discount_rate must be between 0 and 100' if discount_rate < 0 || discount_rate > 100
price - (price * discount_rate / 100)
end
end
対応する spec
require_relative '../discount_calculator'
RSpec.describe DiscountCalculator do
describe '.price_after_discount' do
it '10%引きの場合は正しい金額を返す' do
expect(DiscountCalculator.price_after_discount(1000, 10)).to eq(900)
end
it '割引率が0の場合は元の金額を返す' do
expect(DiscountCalculator.price_after_discount(1000, 0)).to eq(1000)
end
it '割引率が100の場合は0を返す' do
expect(DiscountCalculator.price_after_discount(1000, 100)).to eq(0)
end
it '割引率が100を超える場合はエラーにする' do
expect {
DiscountCalculator.price_after_discount(1000, 120)
}.to raise_error(ArgumentError)
end
it '割引率が0未満の場合はエラーにする' do
expect {
DiscountCalculator.price_after_discount(1000, -1)
}.to raise_error(ArgumentError)
end
end
end
単体テストを書くときは、次の観点で整理すると考えやすいです。
正常系
想定どおりの入力で、想定どおりの結果になるかを確認します。
例:
- 正しい名前を入力したら保存できる
- 10%引きなら900円になる
異常系
不正な入力や想定外の条件でも、安全に動くかを確認します。
例:
- 必須項目が空なら無効になる
- 不正な引数ならエラーを返す
- nil が来ても落ちないようにする
境界値
境目の値で不具合が出ないかを確認します。
例:
- 0件
- 1件
- 最大件数
- 100%
- 最小値、最大値
以上の3点を意識すると整理しやすいです。
変更したspecだけ実行したい場合
開発中は毎回すべてのテストを実行すると時間がかかることがあります。
そのため、まずは 変更した spec ファイルだけ実行 することが多いです。
bundle exec rspec spec/models/user_spec.rb
1行番号を指定して、そのテストだけ実行することもできます。
bundle exec rspec spec/models/user_spec.rb:10
これにより、修正中の確認がしやすくなります。
最終的にはまとめて自動実行する
開発では、最終的に 複数の spec をまとめて自動実行 します。
これは、今回の修正が他の機能に影響していないか確認するためです。
bundle exec rspec
実務では、よく次の流れで進めます。
- 修正する
- 関連する spec を追加・修正する
- 変更した spec を個別実行する
- 問題なければ全体実行する
CIとの関係
単体テストは、ローカルで実行するだけでなく、GitHub Actions などの
CI(継続的インテグレーション) でも自動実行されることが多いです。
つまり、コードを push したり Pull requests を作成したりしたタイミングで、自動的にテストが走ります。
これにより、次のようなメリットがあります。
- 壊れたコードの取り込みを防ぐ
- レビュー前に最低限の品質を確認できる
- メンバー全員が同じ基準で確認できる
単体テストを書くときのポイント
1. 1つのテストで1つの観点を確認する
1つのテストに多くの確認を詰め込みすぎると、失敗理由がわかりにくくなります。
「正常系」「異常系」「境界値」など、観点を分けて書くのが基本です。
2. 実装ではなく振る舞いを確認する
内部の細かい処理よりも、入力と出力の関係を確認するほうが、変更に強いテストになります。
3. まず重要なロジックから書く
すべてを最初から完璧にテストしようと、
計算処理、判定処理、バリデーションなど、壊れると困る部分から書くのが実践的です。
まとめ
単体テストの目的は、コードが期待どおりに動くことを小さな単位で確認し、修正時の不具合を防ぐことです。
基本的には以下の流れを押さえれば十分始められます。
- テストしたいクラスやメソッドを決める
- 期待する結果を書く
-
bundle exec rspecで実行する - 失敗したら原因を直す
- 修正後も繰り返し実行する
単体テストは、単に動作確認を自動化するものではありません。
修正時に安心してコードを変更するための土台であり、
特に Ruby / Rails 開発では、RSpec を使って振る舞いを明確に記述することで、仕様の見える化と品質維持の両方に役立ちます。