はじめに (対象読者・この記事でわかること)

この記事は、Javaでユニットテストを記述しており、特にJMockitを使用してモック化を行っている開発者の方々を主な対象としています。テスト実行時に「Warning: Redundant recording」というメッセージがコンソールに頻繁に表示され、そのためにログが見づらい、重要な情報を見落としかねない、と感じている方々に特にお読みいただきたい内容です。

この記事を読むことで、JMockitの「Redundant recording」警告がなぜ発生するのか、その根本的な意味を深く理解できます。また、この警告を根本的に解決するためのテストコードの改善方法、そして一時的または特定の状況下で出力を抑制するためのJVMオプション設定やロギングフレームワークによる制御方法について、具体的な手順とコード例を交えて習得できます。

大規模なプロジェクトでJMockitを使ったテストコードが増えるにつれ、この警告が大量に出力され、重要なエラーメッセージを見逃したり、CI/CDログの可読性が著しく低下したりする問題に直面することがあります。そこで、この問題を効果的に解決するための知見を共有したく、本記事を執筆しました。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 * Javaの基本的な文法とオブジェクト指向プログラミングの概念 * JUnitなどのテストフレームワークと、ユニットテストの基本的な書き方 * JMockitの基本的な使い方(@Mocked, Expectations, Verificationsなどのアノテーションやブロックの概念)

JMockitの「Warning: Redundant recording」警告とは?

JMockitを使用していると、テスト実行時にコンソールにWarning: Redundant recordingというメッセージが出力されることがあります。この警告は、Expectationsブロック内で定義されたモックの振る舞いの中に、実際にテスト対象のコード内で呼び出されていない(=冗長な)記録が含まれていることを示しています。

例えば、以下のようなシナリオで発生します。

  1. Expectationsブロック内で、モックオブジェクトのメソッドmethodA()methodB()が呼び出されると期待する振る舞いを記録したとします。
  2. しかし、実際のテスト対象のコードではmethodA()しか呼び出されず、methodB()は一度も呼び出されなかったとします。

この場合、methodB()の記録は「Redundant recording(冗長な記録)」と見なされ、警告が出力されます。これは直接的なエラーではありませんが、テストコードが意図せず複雑になっている、あるいは将来的に不要な期待値定義が残っている可能性を示唆しています。この警告が多発すると、テスト結果のログが埋もれてしまい、本当に重要な情報を見落とすリスクが高まります。テストコードの品質低下や保守性の悪化にも繋がるため、適切な対応が望まれます。

コンソール出力抑制の具体的な方法

「Redundant recording」警告の抑制には、根本的な解決から一時的な対応までいくつかの方法があります。ここでは、それぞれのアプローチとその手順を解説します。

ステップ1: テストコードを修正し、冗長な記録を排除する(推奨)

最も推奨されるのは、警告の根本原因である冗長な記録をテストコードから排除することです。これにより、テストコードの可読性と保守性が向上し、意図しない振る舞いを記録してしまうリスクを低減できます。

冗長な記録とは?

Expectationsブロックは、テスト対象メソッドがモックオブジェクトに対してどのようなメソッドをどのような引数で、何回呼び出すかを記述する場所です。もし、テスト対象メソッドが呼び出さないモックメソッドの振る舞いをExpectationsブロック内に記述してしまうと、それは冗長な記録となります。

Before (冗長な記録の例): ここでは、MyServiceMyDependencyインターフェースに依存していると仮定します。

Java
// MyService.java (テスト対象クラス) public class MyService { private MyDependency dependency; public MyService(MyDependency dependency) { this.dependency = dependency; } public String processData(String input) { // 実際にはdependency.getData()のみを呼び出す String data = dependency.getData(input); // dependency.transform(data) はこのメソッドでは呼び出されない return "Processed: " + data; } } // MyDependency.java (依存インターフェース) public interface MyDependency { String getData(String input); String transform(String data); // このメソッドはprocessDataでは使われない } // MyServiceTest.java (テストコード - 冗長な記録を含む) import mockit.Expectations; import mockit.Mocked; import mockit.Verifications; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class MyServiceTest { @Mocked MyDependency mockDependency; @Test public void testProcessDataWithRedundantRecording() { new Expectations() {{ mockDependency.getData("test_input"); result = "mockedData"; // !!! この行が冗長な記録となる !!! // MyService.processDataではこのメソッドは呼び出されない mockDependency.transform("mockedData"); result = "transformedData"; }}; MyService service = new MyService(mockDependency); String result = service.processData("test_input"); assertEquals("Processed: mockedData", result); new Verifications() {{ mockDependency.getData("test_input"); times = 1; // mockDependency.transform("mockedData"); は検証されないため、 // 記録されているとJMockitが警告を発する。 }}; } }

上記の例では、MyService.processDataメソッドはMyDependency.getDataしか呼び出さないにも関わらず、Expectationsブロック内でmockDependency.transformの振る舞いを記録しています。これが「Redundant recording」警告の原因となります。

After (冗長な記録を修正した例): Expectationsブロックから不要な記録を削除するだけで、警告は解消されます。

Java
// MyServiceTest.java (テストコード - 修正後) import mockit.Expectations; import mockit.Mocked; import mockit.Verifications; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; public class MyServiceTest { @Mocked MyDependency mockDependency; @Test public void testProcessDataClean() { new Expectations() {{ // 実際に呼び出される振る舞いのみを記録する mockDependency.getData("test_input"); result = "mockedData"; }}; MyService service = new MyService(mockDependency); String result = service.processData("test_input"); assertEquals("Processed: mockedData", result); new Verifications() {{ mockDependency.getData("test_input"); times = 1; }}; } }

このように、Expectationsブロックから不要な記録を削除することで、警告は解消され、テストコードの意図がより明確になります。テストコードは、実際にテスト対象が必要とするモックの振る舞いだけを記述するように心がけましょう。

ステップ2: JVMオプションでJMockitのログレベルを変更する

テストコードの修正がすぐに難しい場合や、特定の環境でのみ警告を抑制したい場合は、JMockitのログレベルをJVMオプションで変更する方法があります。これにより、警告レベル以下のメッセージが出力されなくなります。

JMockitは内部的にロギングフレームワークを使用せず、System.errに直接出力することが多いため、jmockit.minLogシステムプロパティを使用して制御します。

設定方法

JVM起動時に-Djmockit.minLogプロパティを設定します。

  • ERROR: ERRORレベル以上のメッセージのみを出力します。WARNINGレベルの「Redundant recording」は抑制されます。
  • INFO: INFOレベル以上のメッセージを出力します。WARNINGレベルは出力されます。
  • WARNING: WARNINGレベル以上のメッセージを出力します。
  • DEBUG: DEBUGレベル以上のメッセージを出力します(最も詳細)。

Redundant recordingWARNINGレベルで出力されるため、これを抑制するにはERRORを設定します。

Bash
# コマンドラインからテストを実行する場合の例 java -Djmockit.minLog=ERROR -jar your-test-runner.jar # Mavenの場合 (pom.xmlの<build>セクション内) <project> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> <!-- またはそれ以降のバージョン --> <configuration> <argLine>-Djmockit.minLog=ERROR</argLine> </configuration> </plugin> </plugins> </build> </project> # Gradleの場合 (build.gradleファイル内) test { // ユニットテスト実行時のJVM引数を設定 jvmArgs '-Djmockit.minLog=ERROR' // その他、必要に応じてテストオプションを設定 useJUnitPlatform() // JUnit 5を使用する場合 }

これらの設定を適用することで、テスト実行時にJMockitによる「Redundant recording」警告がコンソールに出力されなくなります。

メリットとデメリット

  • メリット: 比較的簡単に設定でき、広範囲にわたるテストに適用できます。開発環境でのみ警告を抑制したい場合などに便利です。
  • デメリット: 「Redundant recording」以外のJMockitが出力する他の重要な警告メッセージ(例: Misplaced expectations or verificationsなど)もすべて抑制されてしまう可能性があります。これにより、本来修正すべき問題を見落とすリスクがあります。あくまで一時的、または警告内容を十分に理解した上で利用するべきです。

ステップ3: ロギングフレームワークでJMockitの出力をフィルタリングする

JMockitのログがSystem.errではなく、間接的にロギングフレームワーク(Logback, Log4j2など)を通じて出力される場合(JMockit自体は直接これらのフレームワークを使わないことが多いですが、例えばテストランナーがSystem.errをキャプチャしてログに流すような場合)、ロギングフレームワークの設定で特定のログカテゴリのレベルを変更することで抑制できる可能性があります。

ただし、JMockitの「Redundant recording」警告は通常、System.errに直接出力されるため、一般的なロギングフレームワークのフィルタリングでは抑制できないことが多い点に注意が必要です。この方法は、JMockitの警告が何らかの理由でロギングフレームワークに取り込まれている特殊なケースでのみ有効です。

Logbackでの設定例 (一般的なロギングの場合)

もしJMockitの警告がjmockit.internalのようなパッケージ名でロギングされていると仮定した場合:

Xml
<!-- src/test/resources/logback-test.xml --> <configuration> <!-- コンソール出力用のAppender --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- jmockit関連のログレベルをERRORに設定して警告を抑制 --> <!-- 注意: JMockitが直接Logbackに出力しない場合、この設定は無効です --> <logger name="jmockit.internal" level="ERROR" /> <logger name="jmockit" level="ERROR" /> <!-- ルートロガーの設定 (デフォルトはINFOレベルでSTDOUTに出力) --> <root level="INFO"> <appender-ref ref="STDOUT" /> </root> </configuration>

この設定では、jmockit.internalパッケージやjmockitパッケージからのログはERRORレベル以上でなければ出力されなくなります。しかし、前述の通りJMockitの警告はSystem.errへの直接出力が多いため、この方法が直接的に効くことは稀です。System.errに出力される警告をロギングフレームワークでキャプチャするには、テストランナーやフレームワーク側での特別な設定が必要になることが多いです。

ハマった点やエラー解決

  • 安易なログ抑制は危険: JVMオプションやロギングフレームワークによる警告の抑制は、根本原因の解決にはなりません。多くの場合、テストコードに冗長な記述があるか、テストの意図が不明確である兆候です。これらの警告を単に非表示にすることで、将来的にデバッグが困難になったり、テストの信頼性が低下したりする可能性があります。
  • JMockitのログ出力の特殊性: JMockitは標準エラー出力(System.err)に直接警告を出すため、LogbackやLog4j2といった一般的なロギングフレームワークのlogger設定だけではコントロールできないことが多いです。この場合、ステップ2の-Djmockit.minLog=ERRORが最も効果的です。
  • 特定のバージョン依存: JMockitのバージョンによっては、ログの出力挙動やminLogプロパティの挙動が異なる可能性があります。必ず公式ドキュメントやリリースノートで確認しましょう。古いJMockitのバージョンではjmockit.quietのようなプロパティが使われていたこともあります。
  • CI/CD環境での挙動: ローカル環境では問題なくても、CI/CD環境でテストを実行した際にログが大量に出力され、ビルドパイプラインのログ解析が困難になることがあります。CI/CD環境では、-Djmockit.minLog=ERRORを設定することが実用的な選択肢となることがあります。ただし、その場合でも定期的にローカル環境で警告が出ないかを確認し、テストコードを改善する努力は続けるべきです。

解決策

まずはステップ1のテストコード修正を最優先で検討し、テストコードをクリーンアップすることが最も健全なアプローチです。これにより、テストコードの品質が向上し、長期的な保守性も確保されます。

これが困難な場合や、短期間での対応が必要な場合は、ステップ2のJVMオプションによる抑制を適用します。この際も、そのデメリット(他の警告を見逃すリスク)を理解した上で慎重に設定してください。

ステップ3は、JMockitの警告がロギングフレームワークにキャプチャされていると確認できた場合にのみ有効です。ほとんどの場合、JMockitの警告はSystem.errに直接出力されるため、JVMオプションでの制御がより一般的で効果的です。

まとめ

本記事では、JMockit利用時に発生する「Warning: Redundant recording」という警告の原因と、その効果的な抑制方法について解説しました。

  • JMockitの警告はテストコードの冗長性を示す: この警告は、Expectationsブロック内で定義されたモックの振る舞いが、実際のテスト対象のコードでは呼び出されていないことを意味します。
  • 最善の解決策はテストコードの修正: 警告の根本的な解決は、Expectationsブロックから不要なモックの振る舞いを削除し、テストコードの可読性と保守性を向上させることです。
  • JVMオプションによる抑制は一時的な手段: -Djmockit.minLog=ERRORというJVMオプションを使用することで、警告メッセージを非表示にできますが、これは他の重要な警告も隠蔽する可能性があるため、慎重に利用すべきです。

この記事を通して、JMockitの警告メッセージの意味を正確に理解し、ご自身のプロジェクトの状況に合わせて最も適切な抑制策を講じられるようになったことでしょう。テストコードの健全性を保ちながら、開発効率を高める一助となれば幸いです。

今後は、JMockitのより高度な機能活用や、複雑なテストシナリオにおけるモック設計のベストプラクティスなどについても記事にする予定です。

参考資料