Rubyのテスト手法として頻繁に登場するmock
とstub
。これらは、テストの際に外部依存をコントロールするために活用されますが、その役割や使い方が似ているため、初心者はどちらを使うべきか迷ってしまうことが多いです。本記事では、mock
とstub
の違いをわかりやすく説明し、それぞれを適切に使い分けるための知識と具体的な手法を紹介します。テストの精度と効率を向上させるために、この二つの概念をしっかりと理解しておきましょう。
`mock`と`stub`とは?
mock
とstub
は、テストのためにデータや振る舞いを一時的に置き換えるためのオブジェクトです。テストコード内で外部依存に頼らず、特定のシナリオを再現するために利用されますが、これらには明確な違いがあります。
stubの役割
stub
は、特定のメソッド呼び出しに対して固定の値を返すために使用します。これにより、テストしたいオブジェクトが期待通りの動作をしているかを確認することができます。stub
は通常、メソッドの戻り値を決めて外部依存を排除するために使われます。
mockの役割
一方、mock
はメソッドの呼び出しそのものを検証するために用いられます。特定のメソッドが正しい引数で呼ばれたかどうか、または適切な回数で呼ばれたかを確認するためのもので、期待通りの振る舞いをしているかをチェックするのに適しています。
このように、mock
とstub
は同じような目的に使われることもありますが、役割と用途には違いがあるため、状況に応じて使い分けることが重要です。
`mock`の詳細と使用例
mock
は、テスト対象のオブジェクトが期待する通りのメソッドを正しく呼び出しているかを確認するためのオブジェクトです。mock
を使用すると、特定のメソッドが呼ばれたかどうか、その際にどの引数が使われたかを検証することができます。これは、オブジェクト間のやり取りをテストしたい場合に特に有効です。
`mock`の特徴
- 呼び出し回数の検証:
mock
は、指定したメソッドが想定通りの回数で呼び出されているかを確認するのに役立ちます。 - 引数の確認:
mock
を使うことで、メソッドが期待する引数で呼び出されたかもチェックできます。
使用例:通知機能のテスト
例えば、ユーザーにメール通知を送信する機能があるとします。テストでは、実際にメールを送信するのではなく、「送信メソッドが呼ばれたか」を確認したい場合があります。このようなケースでmock
が活躍します。
require 'rspec'
class Notifier
def send_email(user)
# メール送信処理
end
end
describe 'Notifier' do
it 'sends email notification to user' do
user = double('User')
notifier = Notifier.new
# `send_email` メソッドの呼び出しを期待する mock 設定
expect(notifier).to receive(:send_email).with(user)
notifier.send_email(user)
end
end
このコードでは、expect
でsend_email
メソッドがユーザーオブジェクトと共に1度呼び出されることを指定しています。テストが成功すれば、send_email
メソッドが適切に呼び出されていることが確認できます。これにより、不要な依存関係を排除しつつ、コードの挙動を正確にテストできます。
`stub`の詳細と使用例
stub
は、テスト対象のメソッドが特定の値を返すように指定するためのオブジェクトです。stub
を使用することで、実際の外部依存や計算ロジックに左右されず、メソッドの戻り値をコントロールしながらテストを進めることができます。stub
は、依存するメソッドの戻り値がテストケースにどう影響するかを調べる際に特に有効です。
`stub`の特徴
- 特定の戻り値を設定:
stub
はメソッドの戻り値を事前に設定することで、外部の動作に依存しないテストを可能にします。 - 副作用を避ける:
stub
を使えば、データベース接続やネットワークアクセスといった外部への依存を除去できるため、テストを高速かつ安全に実行できます。
使用例:ユーザーのステータス取得のテスト
例えば、ユーザーのログインステータスを取得する機能があるとします。通常であれば、ユーザーのステータスはデータベースや外部の認証システムに依存するため、テストではstub
を使用して特定の戻り値を設定することで、外部依存を排除できます。
require 'rspec'
class User
def logged_in?
# 実際のログイン判定処理
end
end
describe 'User' do
it 'returns true if the user is logged in' do
user = User.new
# `logged_in?` メソッドの戻り値を true に固定する stub 設定
allow(user).to receive(:logged_in?).and_return(true)
# `logged_in?` が true になることを期待
expect(user.logged_in?).to eq(true)
end
end
このテストでは、allow
を使ってlogged_in?
メソッドがtrue
を返すように設定しています。このようにstub
を使えば、外部リソースへの依存を排除して、メソッドが期待通りの戻り値を返すかどうかを簡単にテストできます。
`mock`と`stub`の違いを理解するためのポイント
mock
とstub
はどちらもテストで使用されるオブジェクトですが、それぞれが持つ役割と用途には重要な違いがあります。ここでは、その違いを理解するための主要なポイントを紹介します。
1. 目的の違い
mock
:メソッドが正しい引数と回数で呼び出されているかを確認するために使用します。主に、動作の検証が目的です。stub
:メソッドが返す値を特定のものに固定するために使用します。主に、データの設定が目的です。
2. テストする対象の違い
mock
は、オブジェクトが正しくメソッドを呼び出しているかをチェックするため、オブジェクト間のインタラクションをテストするのに適しています。stub
は、オブジェクトのメソッドが予想通りの結果を返すかどうかをテストする際に使用されます。ビジネスロジックや計算結果のテストに有効です。
3. 使用方法の違い
mock
:expect
メソッドを使って、メソッドの呼び出しや引数を指定します。stub
:allow
メソッドを使って、メソッドが返す値を指定します。
4. 使い分けの判断基準
mock
が適切な場面:システムやオブジェクト間の相互作用をテストしたい場合に使います。例えば、あるメソッドが呼び出されたかを確認したい場合に有効です。stub
が適切な場面:データ依存のメソッド結果が必要な場合や、計算ロジックの結果に影響を与えないテストを行いたい場合に使用します。
mock
とstub
の違いを明確に理解することで、テストの目的に応じた適切なテスト方法を選択でき、テストコードの可読性や保守性も向上します。
`mock`を使うべきシチュエーション
mock
は、オブジェクト間のメソッド呼び出しやインタラクションが正しく行われているかを検証する際に特に役立ちます。以下のようなケースでは、mock
の使用が効果的です。
1. 外部サービスや他のオブジェクトと連携する場合
外部APIや他のクラスへの呼び出しを伴う機能をテストする際には、mock
を使うと効率的です。例えば、ユーザーがログインした際に認証サービスが呼ばれるかをテストする場合、実際のAPIを使用するのではなく、mock
で呼び出しを確認することで、APIコールを再現せずにテストができます。
# 例: 認証サービスをモック化して、呼び出しを確認
expect(auth_service).to receive(:authenticate).with(user)
2. メールや通知などの副作用が発生するメソッドの確認
メール送信やプッシュ通知など、副作用を伴うメソッドをテストする際には、実際の動作は行わず、呼び出しのみを確認することが重要です。mock
でメソッドが呼ばれたかを確認することで、意図した処理が行われているかを安全にテストできます。
3. 特定のメソッドが正しい引数で呼ばれることを確認したい場合
mock
は、メソッドが期待通りの引数で呼ばれるかどうかを検証するのにも有用です。例えば、関数に特定のオプションが渡されているかをテストしたい場合、mock
で引数の内容をチェックできます。
# 例: 特定の引数でメソッドが呼ばれることを期待
expect(logger).to receive(:log).with("INFO", "Process started")
4. メソッドの呼び出し回数をチェックしたい場合
繰り返し呼ばれるメソッドや、1回だけ実行されるべきメソッドの呼び出し回数を確認したい場合もmock
が便利です。呼び出し回数が期待通りでない場合にエラーを発生させることで、予期しないメソッドの多重呼び出しを防ぎます。
# 例: メソッドが一度だけ呼び出されることを期待
expect(service).to receive(:process_data).once
これらのシチュエーションでmock
を利用することで、外部の依存を排除しながら、オブジェクト間の連携が正しく機能しているかを精確にテストできます。
`stub`を使うべきシチュエーション
stub
は、テストの際にメソッドの戻り値を特定の値に固定するために使用されます。これにより、外部リソースや副作用のあるメソッド呼び出しを排除し、テスト対象のロジックに集中することができます。以下のような場面でstub
の利用が適しています。
1. 外部APIやデータベースの依存を排除したい場合
テスト環境で外部APIやデータベース接続を使うと、テストが不安定になる可能性があります。stub
を利用することで、外部依存を排除し、決まった値を返すことで簡単かつ迅速なテストが可能になります。
# 例: データベース呼び出しをスタブ化
allow(user).to receive(:fetch_data).and_return("Test Data")
2. 複雑な計算や処理結果を固定したい場合
重い計算処理や複雑な処理の結果を固定しておくことで、テストの実行速度を向上させることができます。戻り値を固定することにより、処理の速度を犠牲にすることなくテストケースがスムーズに実行されます。
# 例: 複雑な処理の結果を固定
allow(calculator).to receive(:compute).and_return(42)
3. 条件分岐によるシナリオをテストしたい場合
メソッドが返す値を状況に応じて変更し、異なる条件分岐やシナリオをテストする際にstub
は役立ちます。例えば、特定の条件でエラーを発生させる場合や異なる返り値でテストを行いたい場合に、stub
を利用して簡単に再現できます。
# 例: 条件に応じて異なる戻り値を設定
allow(service).to receive(:status).and_return("success", "failure")
4. デフォルトの戻り値や副作用を持つメソッドをテストする場合
システム内で特定の初期値や状態を設定しているメソッドがある場合、その戻り値を固定して、テスト環境で予期せぬ副作用を避けることができます。これにより、テストが確実に実行される環境が整えられます。
# 例: デフォルトの戻り値を指定
allow(config).to receive(:default_timeout).and_return(30)
これらのシチュエーションでstub
を使うことで、テストの環境をコントロールし、外部要因に影響されない安定したテストの実行が可能になります。
`mock`と`stub`の併用方法
複雑なテストシナリオでは、mock
とstub
を併用することで、より柔軟で詳細なテストを行うことが可能です。mock
でメソッド呼び出しの確認を行いながら、stub
で返り値を固定することで、シナリオの再現性を高めつつオブジェクトの振る舞いを精密にチェックできます。以下に、併用すべき場面や具体的な実装例を示します。
1. メソッド呼び出しの確認と戻り値の制御を同時に行いたい場合
例えば、ユーザーのログイン処理をテストする際、認証メソッドが呼ばれたかどうか(mock
)、およびその認証メソッドが特定のユーザーIDを返すこと(stub
)を同時に確認できます。
class Authenticator
def login(user)
# ログイン処理
end
def authenticate
# 認証処理
end
end
describe 'Authenticator' do
it 'authenticates and logs in user' do
authenticator = Authenticator.new
user = double('User', id: 1)
# authenticate メソッドの呼び出しを期待(mock)
expect(authenticator).to receive(:authenticate).with(user)
# login メソッドの戻り値を指定(stub)
allow(authenticator).to receive(:login).and_return(true)
authenticator.authenticate(user)
expect(authenticator.login(user)).to eq(true)
end
end
ここでは、authenticate
メソッドの呼び出しと、login
メソッドの戻り値の制御を同時にテストしています。
2. 外部APIやサービス呼び出しが含まれる複雑なシナリオのテスト
例えば、注文処理をテストする場合、支払いAPIにリクエストが送信されるかを確認し(mock
)、APIのレスポンスを固定値として指定することで(stub
)、注文処理のテストを安定して行えます。
class Order
def process(payment_gateway)
payment_gateway.charge
# 注文処理
end
end
describe 'Order' do
it 'processes order with payment' do
order = Order.new
payment_gateway = double('PaymentGateway')
# charge メソッドの呼び出しを期待(mock)
expect(payment_gateway).to receive(:charge)
# charge メソッドの戻り値を指定(stub)
allow(payment_gateway).to receive(:charge).and_return("Payment Success")
order.process(payment_gateway)
end
end
この例では、支払いAPIのcharge
メソッドの呼び出しを確認しつつ、stub
で戻り値を固定しています。
3. 予期しないエラーをテストする際の併用
複雑なシステムで、エラーが発生した際の動作を検証するために、メソッド呼び出しとともに戻り値を設定できます。たとえば、APIの呼び出しでエラーが発生した場合に、別のメソッドが正しく呼ばれるかを確認することが可能です。
class Service
def execute
# 処理実行
end
def on_error
# エラー処理
end
end
describe 'Service' do
it 'calls error handler on failure' do
service = Service.new
# execute メソッドの呼び出しがエラーを返すように設定(stub)
allow(service).to receive(:execute).and_raise("Execution failed")
# on_error メソッドが呼ばれることを期待(mock)
expect(service).to receive(:on_error)
begin
service.execute
rescue
service.on_error
end
end
end
このコードでは、execute
メソッドがエラーを発生させる場合にon_error
メソッドが呼ばれることを確認するテストを行っています。
これらのように、mock
とstub
を併用することで、複雑な状況においてもテストの精度を高めることができます。状況に応じて適切に併用することで、テストの再現性と信頼性を向上させましょう。
応用例:実践的なテストケース
ここでは、mock
とstub
を組み合わせた実践的なテストケースを紹介します。この応用例では、複数の依存関係が存在するサービスのテストシナリオを構築し、実際の処理を行わずにサービスの呼び出し確認と戻り値のコントロールを実現します。
シナリオ:ECサイトの注文処理テスト
このテストケースでは、ECサイトの注文処理機能が次のような流れで実行されることを確認します。
- 注文データを取得
- 支払い処理を実行
- 注文完了メールをユーザーに送信
この流れを、支払い処理の実行確認や、外部サービス(メール送信)が成功したかを含めて検証します。
class OrderService
def initialize(payment_gateway, mailer)
@payment_gateway = payment_gateway
@mailer = mailer
end
def process_order(order)
if @payment_gateway.charge(order.amount)
@mailer.send_confirmation(order.user_email)
"Order processed successfully"
else
"Payment failed"
end
end
end
テストケース
このOrderService
クラスをテストする際に、以下の条件を満たすことを確認します。
- 支払い処理が行われること
- 支払いが成功した場合にのみ、確認メールが送信されること
- メール送信サービスは実行せず、呼び出し確認のみを行うこと
実装例
require 'rspec'
describe 'OrderService' do
it 'processes the order and sends confirmation email if payment succeeds' do
# 注文オブジェクトの準備
order = double('Order', amount: 100, user_email: 'user@example.com')
# 支払いゲートウェイとメールサービスをダブル化
payment_gateway = double('PaymentGateway')
mailer = double('Mailer')
# 支払いゲートウェイの `charge` メソッドの戻り値を true に固定(stub)
allow(payment_gateway).to receive(:charge).with(order.amount).and_return(true)
# `send_confirmation` メソッドの呼び出しを期待(mock)
expect(mailer).to receive(:send_confirmation).with(order.user_email)
# OrderService インスタンスを作成して処理を実行
service = OrderService.new(payment_gateway, mailer)
result = service.process_order(order)
# 処理結果の確認
expect(result).to eq("Order processed successfully")
end
it 'does not send confirmation email if payment fails' do
order = double('Order', amount: 100, user_email: 'user@example.com')
payment_gateway = double('PaymentGateway')
mailer = double('Mailer')
# 支払いゲートウェイの `charge` メソッドの戻り値を false に固定(stub)
allow(payment_gateway).to receive(:charge).with(order.amount).and_return(false)
# メール送信が呼ばれないことを確認(mock)
expect(mailer).not_to receive(:send_confirmation)
service = OrderService.new(payment_gateway, mailer)
result = service.process_order(order)
expect(result).to eq("Payment failed")
end
end
解説
- 支払い処理の結果を
stub
で固定して、支払いが成功する場合と失敗する場合の両方のシナリオをテストしています。 - 支払いが成功した場合のみ、確認メールが送信されるように
mock
でメール送信の呼び出し確認を行っています。 - 支払いが失敗した場合は、メール送信が行われないことを
not_to receive
でチェックしています。
このように、mock
とstub
を組み合わせることで、注文処理の一連の流れや、成功・失敗の異なるシナリオを詳細にテストできます。外部の依存を除外しながらロジックをテストすることで、テストが安全かつ効率的に行えるため、実際の業務システムに応用する際にも非常に有用です。
まとめ
本記事では、Rubyにおけるmock
とstub
の違いと、それぞれの適切な使い分け方法について解説しました。mock
はメソッドの呼び出しや引数の検証に使用し、stub
はメソッドの戻り値を制御することでテスト環境を整える際に活用します。併用することで複雑なシナリオにも柔軟に対応でき、テストの精度が向上します。これらの知識を活かし、信頼性の高いテストコードを作成して、Rubyプロジェクトの品質向上に役立てましょう。
コメント