クラスの方法におけるモッキートスパイのパイル故障の問題を解決する方法:依存噴射のための実用的なガイド
问题剖析:Mockito Spy桩化失效的根源
在使用Mockito进行单元测试时,spy(监控)功能允许我们对真实对象进行部分模拟,即保留对象原有的行为,同时可以对特定方法进行桩化(stubbing)或验证(verification)。然而,一个常见的误区可能导致 spy 的桩化设置失效,使得测试代码预期获得桩化值,实际却调用了真实方法并返回其默认值或实际计算结果。
问题的核心在于,如果你的生产代码(即被测试的代码)在内部自行创建了 spy 对象所代表的类实例,那么你在测试代码中创建并桩化的 spy 实例将不会被生产代码使用。生产代码将操作一个全新的、未经桩化的实例。
考虑以下代码示例:
依赖类 (GetOptionBidPrice.java)
// 假设这是一个负责获取期权投标价格的类 public class GetOptionBidPrice { public double getBidPrice() { // 实际的业务逻辑,可能涉及网络请求或复杂计算 System.out.println("调用了 GetOptionBidPrice 的真实 getBidPrice() 方法"); return 0.0; // 假设真实方法默认返回0.0 } }
被测试的业务逻辑类 (MyService.java) - 错误示范
public class MyService { // 这种方法内部直接创建 GetOptionBidPrice 实例 public double calculateTotalBidPriceProblematic() { GetOptionBidPrice getOptionBidPrice = new GetOptionBidPrice(); // 问题所在:内部自行创建实例 double bidPrice = getOptionBidPrice.getBidPrice(); return bidPrice * 2; } }
测试代码 (MyServiceTest.java) - 桩化失效的场景
import org.junit.jupiter.api.Test; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; public class MyServiceTest { @Test void testCalculateTotalBidPriceProblematic_StubbingFails() { // 尝试对 GetOptionBidPrice 进行 spy 并桩化 GetOptionBidPrice spyGetOptionBidPrice = spy(new GetOptionBidPrice()); doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice(); // 桩化 getBidPrice 方法返回 100.0 MyService myService = new MyService(); // 调用被测试方法 double result = myService.calculateTotalBidPriceProblematic(); // 预期结果应为 100.0 * 2 = 200.0,但实际会是 0.0 * 2 = 0.0 // 因为 myService 内部创建了一个新的 GetOptionBidPrice 实例,而不是使用了 spyGetOptionBidPrice System.out.println("实际结果: " + result); assertEquals(0.0, result, "桩化未生效,真实方法被调用"); // 断言会失败,因为结果是0.0 verify(spyGetOptionBidPrice, never()).getBidPrice(); // 验证 spy 上的方法从未被调用 } }
在上述示例中,尽管我们在测试中对 spyGetOptionBidPrice 进行了桩化,但 MyService 内部的 calculateTotalBidPriceProblematic 方法却创建了它自己的 GetOptionBidPrice 实例。因此,MyService 操作的是一个全新的、未经桩化的对象,导致我们的桩化设置完全失效。
解决方案:拥抱依赖注入
解决这一问题的核心思想是依赖注入(Dependency Injection, DI)。依赖注入是一种设计模式,它允许一个对象接收其所依赖的其他对象,而不是在内部自行创建这些依赖。这极大地提高了代码的模块化、可测试性和可维护性。
通过依赖注入,我们可以在测试时将我们准备好的 spy 实例“注入”到被测试的对象中,从而确保被测试对象使用的是我们期望的、已被桩化的实例。
被测试的业务逻辑类 (MyService.java) - 正确示范
public class MyService { // 通过构造函数或方法参数注入 GetOptionBidPrice 实例 // 推荐使用构造函数注入,使依赖关系更明确 private final GetOptionBidPrice getOptionBidPrice; // 构造函数注入 public MyService(GetOptionBidPrice getOptionBidPrice) { this.getOptionBidPrice = getOptionBidPrice; } // 或者使用方法参数注入(适用于单次操作的依赖) public double calculateTotalBidPriceCorrect(GetOptionBidPrice getOptionBidPrice) { double bidPrice = getOptionBidPrice.getBidPrice(); return bidPrice * 2; } // 如果是构造函数注入,方法体如下 public double calculateTotalBidPriceUsingInjectedInstance() { double bidPrice = this.getOptionBidPrice.getBidPrice(); return bidPrice * 2; } }
测试代码 (MyServiceTest.java) - 桩化成功的场景
import org.junit.jupiter.api.Test; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; public class MyServiceTest { @Test void testCalculateTotalBidPriceCorrect_StubbingSuccess() { // 1. 创建 GetOptionBidPrice 的真实实例(如果 spy 需要基于真实对象) GetOptionBidPrice realGetOptionBidPrice = new GetOptionBidPrice(); // 2. 创建 spy 实例 GetOptionBidPrice spyGetOptionBidPrice = spy(realGetOptionBidPrice); // 3. 对 spy 实例的特定方法进行桩化 doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice(); // 桩化 getBidPrice 方法返回 100.0 // 4. 实例化 MyService,并通过依赖注入传入 spy 实例 MyService myService = new MyService(spyGetOptionBidPrice); // 使用构造函数注入 // 5. 调用被测试方法 double result = myService.calculateTotalBidPriceUsingInjectedInstance(); // 6. 验证和断言 verify(spyGetOptionBidPrice, times(1)).getBidPrice(); // 验证 spy 上的 getBidPrice 方法被调用了一次 assertEquals(200.0, result, "桩化成功,结果符合预期"); // 断言成功,结果为 200.0 System.out.println("实际结果: " + result); } @Test void testCalculateTotalBidPriceCorrect_MethodInjectionSuccess() { // 1. 创建 GetOptionBidPrice 的真实实例 GetOptionBidPrice realGetOptionBidPrice = new GetOptionBidPrice(); // 2. 创建 spy 实例 GetOptionBidPrice spyGetOptionBidPrice = spy(realGetOptionBidPrice); // 3. 对 spy 实例的特定方法进行桩化 doReturn(100.0).when(spyGetOptionBidPrice).getBidPrice(); // 4. 实例化 MyService (此时可以无参构造,如果 MyService 还有其他依赖) MyService myService = new MyService(new GetOptionBidPrice()); // 假设 MyService 构造函数需要一个 GetOptionBidPrice,这里传入一个普通实例 // 或者 MyService 也可以有无参构造,如果它不依赖 GetOptionBidPrice 除非通过方法注入 // 5. 调用被测试方法,并通过方法参数注入 spy 实例 double result = myService.calculateTotalBidPriceCorrect(spyGetOptionBidPrice); // 6. 验证和断言 verify(spyGetOptionBidPrice, times(1)).getBidPrice(); assertEquals(200.0, result, "桩化成功,结果符合预期"); System.out.println("实际结果: " + result); } }
通过依赖注入,我们成功地将测试中准备好的 spy 实例传递给了 MyService,确保了 MyService 在执行业务逻辑时,调用的是我们已经桩化的 GetOptionBidPrice 实例,从而使测试能够准确地验证预期行为。
注意事项与最佳实践
-
何时使用 spy vs mock:
- mock: 当你需要完全模拟一个对象,不关心其真实行为,或者真实行为难以测试(如外部服务调用、数据库操作)时,使用 mock。mock 对象默认所有方法都不执行真实逻辑,并返回默认值(如 null、0、false)。
- spy: 当你需要部分模拟一个真实对象时使用 spy。spy 对象会执行真实方法,除非你明确地桩化了特定方法。它适用于测试那些复杂对象中只有少数方法需要被控制的场景。
-
依赖注入的重要性:
- 提高可测试性: 这是依赖注入最直接的优势。通过注入依赖,你可以轻松地在测试中替换真实依赖为模拟或桩化对象。
- 降低耦合度: 对象不再负责创建其依赖,而是接收它们,这使得组件之间更加独立。
- 提高可维护性: 依赖关系清晰,更易于理解和修改。
- 促进测试驱动开发(TDD): 在编写代码之前考虑如何测试,自然会倾向于设计易于注入依赖的组件。
- 避免在生产代码中直接实例化依赖: 除非依赖是一个简单的、无状态的工具类,并且其行为在任何情况下都是确定且无副作用的,否则应尽量避免在方法或类内部直接 new 依赖对象。这会使得单元测试变得困难,因为你无法替换这些内部创建的依赖。
- 构造函数注入是首选: 对于一个类必需的依赖,通常推荐使用构造函数注入。这使得类的依赖关系在对象创建时就明确可见,并且保证了对象在创建后处于有效状态。
通过理解 spy 的工作原理并结合依赖注入的最佳实践,我们可以构建出更健壮、更易于测试和维护的Java应用程序。
以上がクラスの方法におけるモッキートスパイのパイル故障の問題を解決する方法:依存噴射のための実用的なガイドの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ホットAIツール

Undress AI Tool
脱衣画像を無料で

Undresser.AI Undress
リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover
写真から衣服を削除するオンライン AI ツール。

Clothoff.io
AI衣類リムーバー

Video Face Swap
完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

人気の記事

ホットツール

メモ帳++7.3.1
使いやすく無料のコードエディター

SublimeText3 中国語版
中国語版、とても使いやすい

ゼンドスタジオ 13.0.1
強力な PHP 統合開発環境

ドリームウィーバー CS6
ビジュアル Web 開発ツール

SublimeText3 Mac版
神レベルのコード編集ソフト(SublimeText3)

HashMapは、Javaのハッシュテーブルを介してキーと値のペアストレージを実装し、そのコアはデータの位置をすばやく配置することにあります。 1.最初にキーのHashCode()メソッドを使用して、ハッシュ値を生成し、ビット操作を介して配列インデックスに変換します。 2。異なるオブジェクトは、同じハッシュ値を生成し、競合をもたらす場合があります。この時点で、ノードはリンクされたリストの形式で取り付けられています。 JDK8の後、リンクされたリストが長すぎ(デフォルトの長さ8)、効率を改善するために赤と黒の木に変換されます。 3.カスタムクラスをキーとして使用する場合、equals()およびhashcode()メソッドを書き直す必要があります。 4。ハッシュマップは容量を動的に拡大します。要素の数が容量を超え、負荷係数(デフォルト0.75)を掛けた場合、拡張して再ハッシュします。 5。ハッシュマップはスレッドセーフではなく、マルチスレッドでconcuを使用する必要があります

オプションは、意図を明確に表現し、ヌルの判断のコードノイズを減らすことができます。 1. optional.ofnullableは、nullオブジェクトに対処する一般的な方法です。たとえば、マップから値を取得する場合、Orelseを使用してデフォルト値を提供できるため、ロジックはより明確かつ簡潔になります。 2.チェーンコールマップを使用してネストされた値を達成してNPEを安全に回避し、リンクが無効である場合はデフォルト値を返す場合は自動的に終了します。 3.フィルターは条件付きフィルタリングに使用でき、その後の操作は条件が満たされた場合にのみ実行され続けます。そうしないと、軽量のビジネス判断に適したOrelseに直接ジャンプします。 4.基本的なタイプや単純なロジックなど、複雑さを高めるなど、オプションを過剰使用することはお勧めしません。一部のシナリオはNUに直接戻ります。

Javaのキャラクターエンコーディングの問題に対処するために、重要なのは、各ステップで使用されるエンコードを明確に指定することです。 1.テキストを読み書きするときは常にエンコードを指定し、inputstreamreaderとoutputStreamWriterを使用し、明示的な文字セットを渡して、システムのデフォルトエンコードに依存しないようにします。 2.ネットワーク境界で文字列を処理するときに両端が一貫していることを確認し、正しいコンテンツタイプのヘッダーを設定し、ライブラリでエンコードを明示的に指定します。 3. string.getBytes()およびNewString(byte [])を注意して使用し、プラットフォームの違いによって引き起こされるデータの破損を避けるために、常に手動でstardantcharsets.utf_8を指定します。要するに、

java.io.notserializableExceptionに遭遇するためのコアワークアウンドは、シリアル化する必要があるすべてのクラスがシリアル化可能なインターフェイスを実装し、ネストされたオブジェクトのシリアル化サポートを確認することです。 1.メインクラスに機器を追加する可能性のあるものを追加します。 2.クラス内の対応するカスタムフィールドのクラスも、シリアル化可能なものを実装していることを確認します。 3.一時的に使用して、シリアル化する必要のないフィールドをマークする。 4.コレクションまたはネストされたオブジェクトの非シリアル化されたタイプを確認します。 5.どのクラスがインターフェイスを実装していないかを確認します。 6.キーデータの保存やシリアル化可能な中間構造の使用など、変更できないクラスの交換設計を検討します。 7.変更を検討してください

Javasocketプログラミングはネットワーク通信の基礎であり、クライアントとサーバー間のデータ交換はソケットを介して実現されます。 1。Javaのソケットは、クライアントが使用するソケットクラスとサーバーが使用するサーバーソケットクラスに分割されます。 2。ソケットプログラムを作成するときは、最初にサーバーリスニングポートを起動してから、クライアントによる接続を開始する必要があります。 3.コミュニケーションプロセスには、接続の確立、データの読み取りと書き込み、ストリームの閉鎖が含まれます。 4.注意事項には、ポート競合の避け、IPアドレスの正確な構成、合理的に閉じるリソース、複数のクライアントのサポートが含まれます。これらをマスターすると、基本的なネットワーク通信機能が実現できます。

Javaでは、Defaultのデフォルトソートルールを内部的に定義するためにAcparableが使用され、コンパレータを使用して複数のソートロジックを外部から定義します。 1.Comparableは、クラス自体によって実装されるインターフェイスです。比較()メソッドを書き換えることにより、自然な順序を定義します。弦や整数など、固定および最も一般的に使用されるソートメソッドを備えたクラスに適しています。 2。Comparatorは、同じクラスに複数の並べ替え方法が必要な状況に適した、Compare()メソッドを介して実装された外部定義の機能インターフェイスであり、クラスソースコードを変更できない、またはソートロジックが変更されることが多い場合があります。 2つの違いは、比較可能がソートロジックを定義するだけで、クラス自体を変更する必要があることですが、比較して

Javaにはマップを通過する3つの一般的な方法があります。1。エントリセットを使用してキーと値を同時に取得します。これは、ほとんどのシナリオに適しています。 2。キーセットまたは値をそれぞれキーまたは値を通過する。 3. Java8のForeachを使用して、コード構造を簡素化します。 EntrySetは、すべてのキー値ペアを含むセットを返し、各ループはキーと値に頻繁にアクセスするのに適したMap.entryオブジェクトを取得します。キーまたは値のみが必要な場合は、それぞれkeyset()またはvalues()を呼び出すことができます。または、キーを横断するときにmap.get(key)を介して値を取得できます。 Java 8はForeachを使用できます((key、value) - &gt

Injava、thestatickeywordmeansameansmestotheclassit self、nottointances.staticvariablesaresharedacrossallinstancesは、Objedcreationを使用して、GlobalTrackingorconconstants.StaticMethododsodsodsoperateateClasslevel
