Mockito spy() と Eclipse デバッガーの相性問題: ブレークポイントが効かない原因と解決策

# 概要
Eclipse IDE で Mockito の spy() を使用する際、デバッガーでブレークポイントが正常に機能しないという問題が報告されています。本記事では、この問題の原因を掘り下げ、具体的な解決策をまとめます。

# 前提知識: そもそも Mockito とか spy() ってなに？
すみませんが本記事では Mockito やその spy() 機能については既知である前提で書いています。そこから知りたいという方はまず下記の記事などを参照いただければ幸いです。

https://developer.mamezou-tech.com/blogs/2023/08/13/using-mockito-basic/

https://qiita.com/rururu_kenken/items/86056a25453e896254be


# 現象: Eclipse デバッガーが spy() で設定したブレークポイントをスキップする

Mockito の spy() を使用したメソッド内にブレークポイントを設定しても、Eclipse デバッガーがそこで停止せず、あたかもブレークポイントが存在しないかのようにテストが実行されてしまう現象が確認されています。

具体的には、以下のような状況で問題が発生する可能性があります。

* **@Spy アノテーションを付与した Bean のメソッド:** Spring のテストなどで @Spy アノテーションを利用している場合
* **Mockito.spy() メソッドで明示的に spy オブジェクトを作成した場合:** ユニットテスト内で Mockito.spy(realObject) のように spy オブジェクトを作成した場合

この問題は、IntelliJ IDEA などの他の IDE では発生しにくいことが報告されており、Eclipse 固有の問題である可能性が高いです。

# 原因: バイトコードインストゥルメンテーションと Eclipse デバッガーのミスマッチ

この問題の根本原因は、Mockito の spy() が内部的に行っている バイトコードインストゥルメンテーション という技術と、Eclipse デバッガーの動作原理との間にミスマッチがあるためです。

## 1. Mockito の spy() の仕組み: バイトコードインストゥルメンテーション

Mockito の spy() は、実際のオブジェクトを基に、動的にサブクラスやプロキシを生成します。Mockito 5 (Java 11 以降) では、デフォルトで インラインモックメーカー という仕組みが使用され、JVM のインストゥルメンテーション API を利用して、実行時にクラスを再定義します。

この動的なクラス再定義により、実行時に動作するクラスは、Eclipse が認識している元のコンパイル済みクラスと完全に一致しなくなります。

【参考記事】

https://qiita.com/cilto/items/b6c3693a542666528477

https://30dayscoding.com/blog/java-bytecode-instrumentation-profiling-monitoring-applications

## 2. 行番号情報の欠落

Mockito が生成する ``` $MockitoMock$... ``` のような名前のクラスは、デバッグに必要な 行番号情報 などのメタデータが欠落している場合があります。Eclipse デバッガーは、設定されたブレークポイントをこれらの生成されたクラスにも適用しようとしますが、行番号情報がないため、ソースコード上のブレークポイントと対応付けることができず、「ブレークポイントをインストールできません」というエラーが発生し、結果としてブレークポイントが無視されてしまいます。

## 3. Spring CGLIB プロキシとの類似性

Spring Framework などのフレームワークも、動的なプロキシ生成に CGLIB などのライブラリを使用しており、同様のデバッグ問題が発生することが知られています。Mockito の spy() も同様のメカニズムを利用しているため、Eclipse デバッガーが混乱してしまうのです。

## 4. 既知の報告
この問題は、以前から多くの開発者によって報告されています。(URLは記事の最後にあります）

* **Eclipse フォーラム (2019年):** Mockito の spy() 導入後にブレークポイントが機能しなくなったという報告。``` $MockitoMock$ ``` クラスの行番号警告が表示された
* **Stack Overflow (2020年):** 「Eclipse Mockito debugging; Doesn’t stop on breakpoints in spied beans」という質問で、@Spy Bean のメソッドでブレークポイントがヒットしない問題が報告された (IntelliJ IDEA では正常)
* **GitHub Issue (Mockito #2743, 2022年):** mockito-inline 使用時にブレークポイントが無視される、ステップ実行が不安定になるなどの報告。Eclipse で「InternalException: Got error code 35…」エラーが発生
* **Eclipse JDT Issue (#234, 2023年):** Eclipse JDT のバグトラッカーでもこの問題が認識されており、Eclipse チームも把握している

これらの報告から、Mockito 5.3.1 と Eclipse (4.21 以降) の組み合わせにおいて、spy() やインラインモックを使用したコードのデバッグに既知の非互換性があることが確認できます。

# 解決策と回避策: ブレークポイントを有効にするために

現時点では、Mockito または Eclipse 自体による公式な修正は提供されていません。しかし、以下の回避策を試すことで、ブレークポイントが機能するようになる可能性があります。
## 1. Spy の初期化後にブレークポイントを設定する

最も確実な回避策の一つは、テスト実行開始後にブレークポイントを設定する方法です。

* テストメソッドの先頭など、spy 対象のメソッドが実行される前に一時的なブレークポイントを設定します。
* デバッグモードでテストを実行します。
* Eclipse が一時的なブレークポイントで停止したら、spy 対象のメソッド内にブレークポイントを追加または有効にします。
* テストの実行を再開します。

この方法により、Mockito のバイトコード操作が完了した後にブレークポイントが設定されるため、Eclipse デバッガーが正しくブレークポイントを認識できるようになります。

## 2. Eclipse の「メソッド結果を表示」オプションを無効にする

Eclipse のデバッグ設定にある「ステップ操作後にメソッド結果を表示する」オプションが、インストゥルメントされたコードのステップ実行を妨げ、JDWP InternalException (エラーコード 35) を引き起こすことがあります。

このオプションを無効にすることで、デバッガーの混乱を軽減できる可能性があります。

* **設定手順:** 「ウィンドウ」 > 「設定」 > 「Java」 > 「デバッグ」 > 「ステップ操作後にメソッド結果を表示する」のチェックを外します。

## 3. 従来の MockMaker (Subclass) を使用する

Mockito のモック作成方法を、デフォルトのインラインモックメーカーから、古い サブクラスベースの MockMaker に切り替えることで、ブレークポイントが機能するようになる場合があります。

サブクラス MockMaker は、メソッドの元のバイトコードを保持したサブクラスプロキシを生成するため、デバッガーがより理解しやすくなる可能性があります。

### 3-1. モックごとの設定:

JUnit 5 の Mockito 拡張機能を使用している場合は、@Mock または @Spy アノテーションで mockMaker = MockMakers.SUBCLASS を指定します。

``` Java:コード例
@Spy(mockMaker = MockMakers.SUBCLASS)
private MyService myService;
```

### 3-2. グローバル設定 (すべてのテストに適用):

プロジェクトのクラスパスに mockito-extensions/org.mockito.plugins.MockMaker というファイルを作成し、ファイルの内容を mock-maker-subclass と記述します。

ただし、サブクラス MockMaker を使用すると、以下の制限事項が発生します。

* ファイナルクラスやファイナルメソッドのモック化が不可
* static メソッドやコンストラクタのモック化 (Mockito 4 以降の新機能) が不可

これらの機能が不要な場合は、サブクラス MockMaker への切り替えを検討する価値があります。

## 4. Eclipse をアップグレードするか、別の IDE を試す

Eclipse のバージョンを最新版にアップグレードすることで、デバッガーの改善が含まれている可能性があります。ただし、最新の Eclipse でも完全に解決されていないという報告もあります。

もし可能であれば、IntelliJ IDEA や VS Code などの別の IDE でデバッグを試すことも有効な手段です。これらの IDE は、Mockito の spy() をより適切に処理できる場合があります。

## 5. 行番号警告を再有効化する (オプション)

以前に「行番号属性がありません」警告を非表示にした場合、「設定」>「Java」>「デバッグ」>「行番号属性がないためにブレークポイントをインストールできない場合に警告する」を再度有効にすることを検討してください。

警告が表示されることで、ブレークポイントがプロキシクラスに正しくアタッチされていないことを認識でき、上記の回避策を適用するきっかけになります。
まとめ: 回避策を活用し、快適なデバッグ環境を

Mockito の spy() と Eclipse デバッガーの相性問題は、Mockito のバイトコードインストゥルメンテーションと Eclipse デバッガーの動作原理のミスマッチによって引き起こされます。

現時点では公式な修正は提供されていませんが、spy 初期化後のブレークポイント設定、Eclipse の設定調整、従来の MockMaker の利用などの回避策を活用することで、問題を回避し、Eclipse で spy() を使用したコードを効果的にデバッグすることが可能です。

Mockito の spy() は非常に強力な機能であり、ユニットテストの品質向上に大きく貢献します。本記事で紹介した回避策を参考に、Mockito と Eclipse を組み合わせて、より快適な開発環境を構築してください。

# 参考情報源
* [Eclipse forum report of $MockitoMock$ missing line numbers and breakpoints acting like run](https://www.eclipse.org/forums/index.php/t/1100229/#:~:text=I%20started%20using%20Mockito%20spy%28,Modify%20compiler%20options)
* [Stack Overflow question on Eclipse not stopping in spied code (user environment & symptoms)​](https://stackoverflow.com/questions/61775881/eclipse-mockito-debugging-doesnt-stop-on-breakpoints-in-spied-beans#:~:text=)
* [Stack Overflow answer describing how adding breakpoints after test start allows debugging of spies​](https://stackoverflow.com/questions/61775881/eclipse-mockito-debugging-doesnt-stop-on-breakpoints-in-spied-beans#:~:text=The%20solution%20I%20happened%20on,breakpoint%20in%20the%20Spy%27d%20class)
* [Mockito GitHub issue #2743 (reports of breakpoints ignored and JDWP errors with mockito-inline, Eclipse & JDK11)​](https://github.com/mockito/mockito/issues/2743)
* [Explanation of missing line numbers in dynamic proxies (Spring Tools Team)​](https://github.com/spring-attic/spring-ide/issues/78)
* [Eclipse JDT debug workaround for “error code 35” (disable method result after step)​](https://stackoverflow.com/questions/45934599/eclipse-oxygen-debugging-issue-step-over#:~:text=go%20to%20Window%20,the%20VM%3B%20may%20be%20slow)
* [Mockito 5 docs – default inline mock maker and using MockMakers.SUBCLASS for classic behavior​](https://davidvlijmincx.com/posts/upgrade-to-mockito-5/#using-the-old-subclass-mockmaker)
* [Older Mockito issue confirming disabling inline mode fixes the breakpoint problem​](https://github.com/mockito/mockito/issues/1488#:~:text=If%20I%20disable%20mock,remove%20final%20as%20a%20workaround)