prepend されたモジュールの private メソッドに対する any_instance スタブの制限

rspec/rspec

RSpec Mocks の any_instance スタブが、prepend されたモジュールで定義された private/protected メソッドを誤って許可していた問題が修正されました。この修正により、prepend を使用したコードでのテスト間の状態漏洩が防止されます。

背景

RSpec Mocks は、prepend されたモジュールで定義されたメソッドに対する any_instance スタブを禁止する設計になっています。prepend されたメソッドをスタブ化すると、スタブの適切なクリーンアップができず、後続のテストに影響が残るためです。しかし、この制限は method_defined? メソッドに基づいていたため、private および protected メソッドを検出できませんでした。

#293 で報告された問題では、prepend された private メソッドに対する any_instance スタブがテスト実行後も残存し、before(:context) ブロックで呼び出された際に "undefined method 'ensure_registered' for RSpec::Mocks::RootSpace" エラーが発生していました。この現象はテストの実行順序に依存するため、不安定なテスト失敗の原因となっていました。

技術的な変更

allow_no_prepended_module_definition_of メソッドの検出ロジックが、可視性に関わらずメソッドを検出できるように拡張されました。

変更前:

def allow_no_prepended_module_definition_of(method_name)
  prepended_modules = RSpec::Mocks::Proxy.prepended_modules_of(@klass)
  problem_mod = prepended_modules.find { |mod| mod.method_defined?(method_name) }
  return unless problem_mod

  AnyInstance.error_generator.raise_not_supported_with_prepend_error(method_name, problem_mod)
end

変更後:

def allow_no_prepended_module_definition_of(method_name)
  prepended_modules = RSpec::Mocks::Proxy.prepended_modules_of(@klass)
  problem_mod = prepended_modules.find do |mod|
    MethodReference.method_defined_at_any_visibility?(mod, method_name)
  end
  return unless problem_mod

  AnyInstance.error_generator.raise_not_supported_with_prepend_error(method_name, problem_mod)
end

MethodReference.method_defined_at_any_visibility? メソッドは、public、protected、private のすべての可視性レベルでメソッドの存在を確認します。これにより、method_defined? では検出できなかった private メソッドと protected メソッドも正しく検出されるようになりました。

テストケースも拡張され、public メソッドだけでなく private/protected メソッドに対する any_instance スタブと receive_message_chain の両方が適切に制限されることを検証しています。追加されたテストは、prepend されたモジュールで定義された各可視性レベルのメソッドに対して、スタブ化の試みが /prepended module/ エラーで失敗することを確認します。

設計判断

既存の method_defined? ベースのチェックを、より包括的な method_defined_at_any_visibility? に置き換える方式が採用されました。

この変更は、検出ロジックの1行の置き換えのみで実装されています。新しいメソッドは MethodReference クラスに既存の機能として存在していたため、追加の実装は不要でした。この判断により、コードの変更を最小限に抑えながら、すべての可視性レベルでの一貫した動作を実現しています。

テストケースの構造も再編成され、可視性レベルごとにコンテキストを分けることで、各ケースでの期待動作が明確になりました。この構造化により、将来的なメソッド可視性の追加や変更に対しても、テストの拡張が容易になります。

まとめ

本PRは、メソッド検出ロジックを可視性非依存の実装に変更することで、prepend パターンを使用したコードでのテスト間の状態漏洩を防止しました。method_defined? から method_defined_at_any_visibility? への置き換えという最小限の変更で、private/protected メソッドに対する any_instance スタブの制限が完全に機能するようになり、テスト実行順序に依存する不安定な失敗が解消されます。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細(各論)→まとめ(結論)の構成が明確で、ガイドラインに準拠しています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトとGitHubのPR/Issueリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

RSpecの内部実装に関する内容であり、専門知識を持つエンジニアという対象読者に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフが「総論→各論」の構造で書かれており、トピックセンテンスが明確です。可読性が非常に高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事で引用されているコードは、提供されたDiff情報と完全に一致しており、ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`prepend`, `any_instance`, `private/protected` などの技術用語がPRの内容に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

「`method_defined?` が private メソッドを検出できない」という問題の根本原因と、`method_defined_at_any_visibility?` による解決策の説明は技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのタイトル、Description、Diffから裏付けられています。「設計判断」セクションも、Diffから読み取れる事実に基づいており、ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#297)とIssue番号(#293)が正確に記載され、リンクされています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRの主題である「prependされたモジュールのprivateメソッドに対するany_instanceの制限」を的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容はすべてPR情報に基づいており、バージョン情報やリリース予定など、PR外の知識の捏造はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「修正されました」「検出されるようになりました」といった時間表現は、完了した変更を報告する文脈として適切です。