Jadual Kandungan
问题剖析:Mockito Spy桩化失效的根源
解决方案:拥抱依赖注入
注意事项与最佳实践
Rumah Java javaTutorial Selesaikan masalah kegagalan longgokan dalam mata -mata mockito dalam kaedah kelas: Panduan Praktikal untuk Suntikan Ketergantungan

Selesaikan masalah kegagalan longgokan dalam mata -mata mockito dalam kaedah kelas: Panduan Praktikal untuk Suntikan Ketergantungan

Aug 04, 2025 pm 08:39 PM

解决Mockito Spy在类方法中桩化失效问题:依赖注入实践指南

本文旨在解决使用Mockito spy对类方法进行桩化(stubbing)时,桩化值未生效反而调用了真实方法的问题。核心原因在于生产代码直接实例化了被监控(spied)对象,而非使用测试中创建的 spy 实例。解决方案是采用依赖注入(Dependency Injection)模式,将 spy 对象作为依赖传递给生产代码,从而确保测试期间桩化行为的正确执行,提升代码的可测试性。

问题剖析: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 实例,从而使测试能够准确地验证预期行为。

注意事项与最佳实践

  1. 何时使用 spy vs mock:
    • mock: 当你需要完全模拟一个对象,不关心其真实行为,或者真实行为难以测试(如外部服务调用、数据库操作)时,使用 mock。mock 对象默认所有方法都不执行真实逻辑,并返回默认值(如 null、0、false)。
    • spy: 当你需要部分模拟一个真实对象时使用 spy。spy 对象会执行真实方法,除非你明确地桩化了特定方法。它适用于测试那些复杂对象中只有少数方法需要被控制的场景。
  2. 依赖注入的重要性:
    • 提高可测试性: 这是依赖注入最直接的优势。通过注入依赖,你可以轻松地在测试中替换真实依赖为模拟或桩化对象。
    • 降低耦合度: 对象不再负责创建其依赖,而是接收它们,这使得组件之间更加独立。
    • 提高可维护性: 依赖关系清晰,更易于理解和修改。
    • 促进测试驱动开发(TDD): 在编写代码之前考虑如何测试,自然会倾向于设计易于注入依赖的组件。
  3. 避免在生产代码中直接实例化依赖: 除非依赖是一个简单的、无状态的工具类,并且其行为在任何情况下都是确定且无副作用的,否则应尽量避免在方法或类内部直接 new 依赖对象。这会使得单元测试变得困难,因为你无法替换这些内部创建的依赖。
  4. 构造函数注入是首选: 对于一个类必需的依赖,通常推荐使用构造函数注入。这使得类的依赖关系在对象创建时就明确可见,并且保证了对象在创建后处于有效状态。

通过理解 spy 的工作原理并结合依赖注入的最佳实践,我们可以构建出更健壮、更易于测试和维护的Java应用程序。

Atas ialah kandungan terperinci Selesaikan masalah kegagalan longgokan dalam mata -mata mockito dalam kaedah kelas: Panduan Praktikal untuk Suntikan Ketergantungan. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn

Alat AI Hot

Undress AI Tool

Undress AI Tool

Gambar buka pakaian secara percuma

Undresser.AI Undress

Undresser.AI Undress

Apl berkuasa AI untuk mencipta foto bogel yang realistik

AI Clothes Remover

AI Clothes Remover

Alat AI dalam talian untuk mengeluarkan pakaian daripada foto.

Clothoff.io

Clothoff.io

Penyingkiran pakaian AI

Video Face Swap

Video Face Swap

Tukar muka dalam mana-mana video dengan mudah menggunakan alat tukar muka AI percuma kami!

Artikel Panas

Panduan pemula ' s ke Rimworld: Odyssey
1 bulan yang lalu By Jack chen
Skop pembolehubah PHP dijelaskan
4 minggu yang lalu By 百草
Petua untuk menulis komen php
3 minggu yang lalu By 百草
Mengulas kod dalam php
3 minggu yang lalu By 百草

Alat panas

Notepad++7.3.1

Notepad++7.3.1

Editor kod yang mudah digunakan dan percuma

SublimeText3 versi Cina

SublimeText3 versi Cina

Versi Cina, sangat mudah digunakan

Hantar Studio 13.0.1

Hantar Studio 13.0.1

Persekitaran pembangunan bersepadu PHP yang berkuasa

Dreamweaver CS6

Dreamweaver CS6

Alat pembangunan web visual

SublimeText3 versi Mac

SublimeText3 versi Mac

Perisian penyuntingan kod peringkat Tuhan (SublimeText3)

Topik panas

Tutorial PHP
1509
276
Bagaimanakah hashmap berfungsi secara dalaman di Java? Bagaimanakah hashmap berfungsi secara dalaman di Java? Jul 15, 2025 am 03:10 AM

HashMap melaksanakan penyimpanan pasangan nilai utama melalui jadual hash di Java, dan terasnya terletak di lokasi data yang cepat. 1. Mula -mula gunakan kaedah hashcode () kunci untuk menghasilkan nilai hash dan mengubahnya menjadi indeks array melalui operasi bit; 2 Objek yang berbeza boleh menghasilkan nilai hash yang sama, mengakibatkan konflik. Pada masa ini, nod dipasang dalam bentuk senarai yang dipautkan. Selepas JDK8, senarai yang dipautkan terlalu panjang (panjang lalai 8) dan ia akan ditukar kepada pokok merah dan hitam untuk meningkatkan kecekapan; 3. Apabila menggunakan kelas tersuai sebagai kunci, sama () dan kaedah hashcode () mesti ditulis semula; 4. HashMap secara dinamik mengembangkan kapasiti. Apabila bilangan elemen melebihi kapasiti dan multiplies oleh faktor beban (lalai 0.75), mengembangkan dan mengembalikan; 5. hashmap tidak selamat benang, dan concu harus digunakan dalam multithreaded

cara menetapkan pembolehubah persekitaran java_home di tingkap cara menetapkan pembolehubah persekitaran java_home di tingkap Jul 18, 2025 am 04:05 AM

Tosetjava_homeonwindows, firstlocatethejdkinstallationpath (mis., C: \ Programfiles \ java \ jdk-17), thencreateasystemenvironmentvaria blenamedjava_homewiththatpath.next, updateThePathvariableByadding%java \ _home%\ bin, andverifythesetupingjava-versionandjavac-v

Penanda aras prestasi benang maya java Penanda aras prestasi benang maya java Jul 21, 2025 am 03:17 AM

Benang maya mempunyai kelebihan prestasi yang signifikan dalam senario yang sangat konkurensi dan intensif, tetapi perhatian harus dibayar kepada kaedah ujian dan senario yang berkenaan. 1. Ujian yang betul harus mensimulasikan perniagaan sebenar, terutamanya senario menyekat IO, dan menggunakan alat seperti JMH atau Gatling untuk membandingkan benang platform; 2. Jurang throughput adalah jelas, dan boleh beberapa kali hingga sepuluh kali lebih tinggi daripada 100,000 permintaan serentak, kerana ia lebih ringan dan cekap dalam penjadualan; 3. Semasa ujian, adalah perlu untuk mengelakkan membabi buta mengejar nombor konvensional yang tinggi, menyesuaikan diri dengan model IO yang tidak menyekat, dan memberi perhatian kepada petunjuk pemantauan seperti latensi dan GC; 4.

Bagaimana menangani transaksi di Java dengan JDBC? Bagaimana menangani transaksi di Java dengan JDBC? Aug 02, 2025 pm 12:29 PM

Untuk mengendalikan transaksi JDBC dengan betul, anda mesti terlebih dahulu mematikan mod komit automatik, kemudian melakukan pelbagai operasi, dan akhirnya melakukan atau mengembalikan semula hasilnya; 1. Panggil Conn.SetAutOcommit (palsu) untuk memulakan transaksi; 2. Melaksanakan pelbagai operasi SQL, seperti memasukkan dan mengemaskini; 3. Panggil Conn.Commit () jika semua operasi berjaya, dan hubungi conn.rollback () jika pengecualian berlaku untuk memastikan konsistensi data; Pada masa yang sama, cuba-dengan-sumber harus digunakan untuk menguruskan sumber, mengendalikan pengecualian dengan betul dan menutup sambungan untuk mengelakkan kebocoran sambungan; Di samping itu, adalah disyorkan untuk menggunakan kolam sambungan dan menetapkan mata simpan untuk mencapai rollback separa, dan menyimpan urus niaga sesingkat mungkin untuk meningkatkan prestasi.

Java Microservices Perkhidmatan Mesh Integrasi Java Microservices Perkhidmatan Mesh Integrasi Jul 21, 2025 am 03:16 AM

ServiceMesh adalah pilihan yang tidak dapat dielakkan untuk evolusi seni bina microservice Java, dan terasnya terletak pada decoupling logik rangkaian dan kod perniagaan. 1. ServiceMesh mengendalikan pengimbangan beban, fius, pemantauan dan fungsi lain melalui agen sidecar untuk memberi tumpuan kepada perniagaan; 2. INTOR ISTIO sesuai untuk projek sederhana dan besar, dan Linkerd lebih ringan dan sesuai untuk ujian berskala kecil; 3. Java microservices harus menutup, reben dan komponen lain dan menyerahkannya kepada Istiod untuk penemuan dan komunikasi; 4. Memastikan suntikan automatik sidecar semasa penempatan, perhatikan konfigurasi peraturan lalu lintas, keserasian protokol, dan pembinaan sistem penjejakan log, dan mengamalkan penghijrahan tambahan dan perancangan pemantauan pra-kawalan.

Melaksanakan senarai yang dipautkan di Java Melaksanakan senarai yang dipautkan di Java Jul 20, 2025 am 03:31 AM

Kunci untuk melaksanakan senarai yang dipautkan adalah untuk menentukan kelas nod dan melaksanakan operasi asas. ①First Buat kelas nod, termasuk data dan rujukan kepada nod seterusnya; ② Kemudian buat kelas LinkedList, melaksanakan fungsi penyisipan, penghapusan dan percetakan; Kaedah tambahan digunakan untuk menambah nod pada ekor; ④ Kaedah PrintList digunakan untuk mengeluarkan kandungan senarai yang dipautkan; ⑤ Kaedah DeletewithValue digunakan untuk memadam nod dengan nilai tertentu dan mengendalikan situasi yang berbeza dari nod kepala dan nod perantaraan.

Bagaimana cara memformat tarikh di Java dengan SimpledateFormat? Bagaimana cara memformat tarikh di Java dengan SimpledateFormat? Jul 15, 2025 am 03:12 AM

Buat dan gunakan SimpledateFormat memerlukan lulus dalam rentetan format, seperti berita yang tidak terkumpul ("Yyyy-mm-ddhh: mm: ss"); 2. Perhatikan kepekaan kes dan elakkan penyalahgunaan format satu huruf bercampur dan Yyyy dan DD; 3. SimpleDateFormat bukanlah benang-selamat. Dalam persekitaran pelbagai thread, anda harus membuat contoh baru atau menggunakan threadlocal setiap kali; 4. Apabila menghuraikan rentetan menggunakan kaedah parse, anda perlu menangkap parseexception, dan perhatikan bahawa hasilnya tidak mengandungi maklumat zon waktu; 5. Adalah disyorkan untuk menggunakan DateTimeFormatter dan Lo

Pengoptimuman Rangka Kerja Koleksi Lanjutan Pengoptimuman Rangka Kerja Koleksi Lanjutan Jul 20, 2025 am 03:48 AM

Untuk meningkatkan prestasi Rangka Kerja Koleksi Java, kami dapat mengoptimumkan dari empat mata berikut: 1. Pilih jenis yang sesuai mengikut senario, seperti akses rawak yang kerap ke ArrayList, carian cepat ke hashset, dan serentak untuk persekitaran serentak; 2. Menetapkan kapasiti dan faktor beban yang munasabah semasa permulaan untuk mengurangkan overhead pengembangan kapasiti, tetapi elakkan sisa memori; 3. Gunakan set yang tidak berubah (seperti list.of ()) untuk meningkatkan keselamatan dan prestasi, sesuai untuk data tetap atau baca sahaja; 4. Mencegah kebocoran memori, dan gunakan rujukan lemah atau perpustakaan cache profesional untuk menguruskan set survival jangka panjang. Butiran ini memberi kesan ketara kepada kestabilan dan kecekapan program.

See all articles