Mengejek variabel anggota kelas menggunakan Mockito

136

Saya seorang pemula untuk pengembangan dan ke unit test pada khususnya. Saya kira kebutuhan saya cukup sederhana, tetapi saya ingin tahu pendapat orang lain tentang ini.

Misalkan saya memiliki dua kelas seperti itu -

public class First {

    Second second ;

    public First(){
        second = new Second();
    }

    public String doSecond(){
        return second.doSecond();
    }
}

class Second {

    public String doSecond(){
        return "Do Something";
    }
}

Katakanlah saya sedang menulis unit test untuk menguji First.doSecond()metode. Namun, misalkan, saya ingin Second.doSecond()kelas Mock seperti itu. Saya menggunakan Mockito untuk melakukan ini.

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Saya melihat bahwa ejekan itu tidak berpengaruh dan pernyataan itu gagal. Apakah tidak ada cara untuk mengejek variabel anggota kelas yang ingin saya uji. ?

Anand Hemmige
sumber

Jawaban:

86

Anda perlu menyediakan cara untuk mengakses variabel anggota sehingga Anda dapat memberikan tiruan (cara yang paling umum adalah metode setter atau konstruktor yang mengambil parameter).

Jika kode Anda tidak memberikan cara untuk melakukan ini, itu salah faktor untuk TDD (Test Driven Development).

kittylyst
sumber
4
Terima kasih. Saya melihatnya. Saya hanya ingin tahu, bagaimana saya kemudian dapat melakukan tes integrasi menggunakan tiruan di mana mungkin ada banyak metode internal, kelas yang mungkin perlu diejek, tetapi belum tentu tersedia untuk ditetapkan melalui setXXX () sebelumnya.
Anand Hemmige
2
Gunakan kerangka kerja injeksi ketergantungan, dengan konfigurasi tes. Buatlah diagram urutan pengujian integrasi yang Anda coba buat. Factor diagram urutan ke dalam objek yang benar-benar dapat Anda kontrol. Ini berarti bahwa jika Anda bekerja dengan kelas kerangka kerja yang memiliki objek anti-pola dependen yang Anda perlihatkan di atas, maka Anda harus menganggap objek dan anggotanya dengan faktor buruk sebagai satu unit dalam hal diagram urutan. Bersiaplah untuk menyesuaikan anjak piutang dari setiap kode yang Anda kontrol, agar lebih dapat diuji.
kittylyst
9
Dear @kittylyst, ya mungkin itu salah dari sudut pandang TDD atau dari segala macam sudut pandang rasional. Tetapi kadang-kadang seorang pengembang bekerja di tempat-tempat di mana tidak ada yang masuk akal sama sekali dan satu-satunya target yang dimiliki hanyalah menyelesaikan cerita yang telah Anda tetapkan dan pergi. Ya, itu salah, tidak masuk akal, orang yang tidak memenuhi syarat mengambil keputusan kunci dan semua itu. Jadi, pada akhirnya, anti-pola menang banyak.
Amanas
1
Saya ingin tahu tentang ini, jika seorang anggota kelas tidak memiliki alasan untuk ditetapkan dari luar, mengapa kita harus membuat setter hanya untuk tujuan mengujinya? Bayangkan kelas 'Kedua' di sini sebenarnya adalah manajer atau alat FileSystem, diinisialisasi selama konstruksi objek yang akan diuji. Saya memiliki semua alasan untuk ingin mengejek manajer FileSystem ini, untuk menguji kelas Pertama, dan tidak ada alasan untuk membuatnya dapat diakses. Saya bisa melakukan ini dengan Python, jadi mengapa tidak dengan Mockito?
Zangdar
65

Ini tidak mungkin jika Anda tidak dapat mengubah kode Anda. Tapi saya suka injeksi ketergantungan dan Mockito mendukungnya:

public class First {    
    @Resource
    Second second;

    public First() {
        second = new Second();
    }

    public String doSecond() {
        return second.doSecond();
    }
}

Tes Anda:

@RunWith(MockitoJUnitRunner.class)
public class YourTest {
   @Mock
   Second second;

   @InjectMocks
   First first = new First();

   public void testFirst(){
      when(second.doSecond()).thenReturn("Stubbed Second");
      assertEquals("Stubbed Second", first.doSecond());
   }
}

Ini sangat bagus dan mudah.

Janning
sumber
2
Saya pikir ini adalah jawaban yang lebih baik daripada yang lain karena InjectMocks.
sudocoder
Lucu bagaimana seseorang, sebagai pemula yang menguji seperti saya, mempercayai perpustakaan dan kerangka kerja tertentu. Saya berasumsi ini hanya Ide Buruk yang menunjukkan kebutuhan untuk merancang ulang ... sampai Anda menunjukkan itu adalah memang mungkin (sangat jelas dan bersih) di Mockito.
mike rodent
9
Apa itu @Resource ?
IgorGanapolsky
3
@IgorGanapolsky the @ Resource adalah anotasi yang dibuat / digunakan oleh kerangka Java Spring. Ini cara untuk menunjukkan ke Spring, ini adalah bean / objek yang dikelola oleh Spring. stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire Ini bukan hal yang mockito, tetapi karena digunakan dalam kelas non-pengujian, ia harus diejek dalam uji.
Grez.Kev
Saya tidak mengerti jawaban ini. Anda mengatakan itu tidak mungkin maka Anda menunjukkan itu mungkin? Apa sebenarnya yang tidak mungkin dilakukan di sini?
Goldname
35

Jika Anda melihat dengan cermat kode Anda, Anda akan melihat bahwa secondproperti dalam pengujian Anda masih merupakan contoh Second, bukan tiruan (Anda tidak meneruskan tiruan ke firstdalam kode Anda).

Cara paling sederhana adalah membuat setter untuk seconddi Firstkelas dan memberikannya mock secara eksplisit.

Seperti ini:

public class First {

Second second ;

public First(){
    second = new Second();
}

public String doSecond(){
    return second.doSecond();
}

    public void setSecond(Second second) {
    this.second = second;
    }


}

class Second {

public String doSecond(){
    return "Do Something";
}
}

....

public void testFirst(){
Second sec = mock(Second.class);
when(sec.doSecond()).thenReturn("Stubbed Second");


First first = new First();
first.setSecond(sec)
assertEquals("Stubbed Second", first.doSecond());
}

Lain akan lulus Secondcontoh sebagai Firstparameter konstruktor.

Jika Anda tidak dapat mengubah kode, saya pikir satu-satunya pilihan adalah menggunakan refleksi:

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");


    First first = new First();
    Field privateField = PrivateObject.class.
        getDeclaredField("second");

    privateField.setAccessible(true);

    privateField.set(first, sec);

    assertEquals("Stubbed Second", first.doSecond());
}

Tapi Anda mungkin bisa, karena jarang melakukan tes pada kode yang tidak Anda kontrol (walaupun orang dapat membayangkan skenario di mana Anda harus menguji perpustakaan eksternal karena penulisnya tidak :))

cek jiwa
sumber
Mengerti. Saya mungkin akan pergi dengan saran pertama Anda.
Anand Hemmige
Hanya ingin tahu, apakah ada cara atau API yang Anda sadari yang dapat mengejek objek / metode di tingkat aplikasi atau tingkat paket. ? Saya kira apa yang saya katakan adalah, dalam contoh di atas ketika saya mengejek objek 'Kedua', apakah ada cara yang dapat menimpa setiap instance dari Kedua yang digunakan melalui siklus pengujian. ?
Anand Hemmige
@AnandHemmige sebenarnya yang kedua (konstruktor) lebih bersih, karena menghindari membuat instance `Second´ yang tidak perlu. Kelas Anda dipisahkan dengan baik seperti itu.
soulcheck
10
Mockito memberikan beberapa anotasi yang bagus untuk memungkinkan Anda menyuntikkan ejekan Anda ke dalam variabel pribadi. Anotasi Kedua dengan @Mockdan anotasi Pertama dengan @InjectMocksdan instantiasi Pertama di penginisialisasi. Mockito akan secara otomatis melakukan yang terbaik untuk menemukan tempat untuk menyuntikkan mock Kedua ke instance Pertama, termasuk pengaturan bidang pribadi yang cocok dengan tipe.
jhericks
@Mocksekitar 1,5 (mungkin lebih awal, saya tidak yakin). 1.8.3 diperkenalkan @InjectMocksjuga @Spydan @Captor.
jhericks
7

Jika Anda tidak dapat mengubah variabel anggota, maka cara lain untuk melakukannya adalah menggunakan powerMockit dan menelepon

Second second = mock(Second.class)
when(second.doSecond()).thenReturn("Stubbed Second");
whenNew(Second.class).withAnyArguments.thenReturn(second);

Sekarang masalahnya adalah bahwa panggilan APAPUN ke Second baru akan mengembalikan contoh yang sama mengejek. Tetapi dalam kasus sederhana Anda ini akan berhasil.

pengguna1509463
sumber
6

Saya memiliki masalah yang sama di mana nilai pribadi tidak ditetapkan karena Mockito tidak memanggil konstruktor super. Inilah cara saya menambah ejekan dengan refleksi.

Pertama, saya membuat kelas TestUtils yang berisi banyak utilitas bermanfaat termasuk metode refleksi ini. Akses refleksi agak sulit untuk diterapkan setiap kali. Saya membuat metode ini untuk menguji kode pada proyek yang, karena satu dan lain alasan, tidak memiliki paket mengejek dan saya tidak diundang untuk memasukkannya.

public class TestUtils {
    // get a static class value
    public static Object reflectValue(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(classToReflect, fieldNameValueToFetch);
            reflectField.setAccessible(true);
            Object reflectValue = reflectField.get(classToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // get an instance value
    public static Object reflectValue(Object objToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameValueToFetch);
            Object reflectValue = reflectField.get(objToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // find a field in the class tree
    public static Field reflectField(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField = null;
            Class<?> classForReflect = classToReflect;
            do {
                try {
                    reflectField = classForReflect.getDeclaredField(fieldNameValueToFetch);
                } catch (NoSuchFieldException e) {
                    classForReflect = classForReflect.getSuperclass();
                }
            } while (reflectField==null || classForReflect==null);
            reflectField.setAccessible(true);
            return reflectField;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch +" from "+ classToReflect);
        }
        return null;
    }
    // set a value with no setter
    public static void refectSetValue(Object objToReflect, String fieldNameToSet, Object valueToSet) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameToSet);
            reflectField.set(objToReflect, valueToSet);
        } catch (Exception e) {
            fail("Failed to reflectively set "+ fieldNameToSet +"="+ valueToSet);
        }
    }

}

Maka saya bisa menguji kelas dengan variabel pribadi seperti ini. Ini berguna untuk mengejek jauh di dalam pohon kelas yang tidak dapat Anda kendalikan.

@Test
public void testWithRectiveMock() throws Exception {
    // mock the base class using Mockito
    ClassToMock mock = Mockito.mock(ClassToMock.class);
    TestUtils.refectSetValue(mock, "privateVariable", "newValue");
    // and this does not prevent normal mocking
    Mockito.when(mock.somthingElse()).thenReturn("anotherThing");
    // ... then do your asserts
}

Saya memodifikasi kode saya dari proyek saya yang sebenarnya di sini, di halaman. Mungkin ada satu atau dua masalah kompilasi. Saya pikir Anda mendapatkan ide umum. Jangan ragu untuk mengambil kode dan menggunakannya jika Anda merasa berguna.

dave
sumber
Bisakah Anda menjelaskan kode Anda dengan usecase yang sebenarnya? seperti Public class tobeMocker () {private ClassObject classObject; } Dimana classObject sama dengan objek yang akan diganti.
Jasper Lankhorst
Dalam contoh Anda, jika ToBeMocker instance = ToBeMocker baru (); dan ClassObject someNewInstance = new ClassObject () {@Override // sesuatu seperti ketergantungan eksternal}; lalu TestUtils.refelctSetValue (misalnya, "classObject", someNewInstance); Perhatikan bahwa Anda harus mencari tahu apa yang ingin Anda ganti untuk mengejek. Katakanlah Anda memiliki database dan penggantian ini akan mengembalikan nilai sehingga Anda tidak perlu memilih. Baru-baru ini saya memiliki bus layanan yang saya tidak ingin benar-benar memproses pesan tetapi ingin memastikan itu menerimanya. Jadi, saya mengatur instance bus pribadi dengan cara ini - Bermanfaat?
dave
Anda harus membayangkan ada pemformatan dalam komentar itu. Itu dihapus. Juga, ini tidak akan berfungsi dengan Java 9 karena akan mengunci akses pribadi. Kami harus bekerja dengan beberapa konstruksi lain begitu kami memiliki rilis resmi dan dapat bekerja dengan batas yang sebenarnya.
dave
1

Banyak orang lain telah menyarankan Anda untuk memikirkan kembali kode Anda untuk membuatnya lebih dapat diuji - saran yang bagus dan biasanya lebih sederhana daripada yang saya sarankan.

Jika Anda tidak dapat mengubah kode untuk membuatnya lebih dapat diuji, PowerMock: https://code.google.com/p/powermock/

PowerMock memperluas Mockito (jadi Anda tidak perlu mempelajari kerangka kerja mock baru), menyediakan fungsionalitas tambahan. Ini termasuk kemampuan untuk memiliki konstruktor mengembalikan tiruan. Kuat, tetapi sedikit rumit - jadi gunakan dengan bijak.

Anda menggunakan pelari Mock yang berbeda. Dan Anda perlu menyiapkan kelas yang akan memanggil konstruktor. (Perhatikan bahwa ini adalah gotcha yang umum - persiapkan kelas yang memanggil konstruktor, bukan kelas yang dibangun)

@RunWith(PowerMockRunner.class)
@PrepareForTest({First.class})

Kemudian dalam set-up pengujian Anda, Anda bisa menggunakan metode whenNew untuk membuat konstruktor mengembalikan tiruan

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));
jwepembelian
sumber
0

Ya, ini bisa dilakukan, seperti yang ditunjukkan tes berikut (ditulis dengan JMockit mocking API, yang saya kembangkan):

@Test
public void testFirst(@Mocked final Second sec) {
    new NonStrictExpectations() {{ sec.doSecond(); result = "Stubbed Second"; }};

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Namun, dengan Mockito, ujian semacam itu tidak dapat ditulis. Ini karena cara mengejek diimplementasikan di Mockito, di mana subclass dari kelas yang akan diejek dibuat; hanya instance dari subclass "mock" ini yang dapat memiliki perilaku mengejek, jadi Anda perlu meminta kode yang diuji menggunakannya alih-alih instance lainnya.

Rogério
sumber
3
pertanyaannya bukan apakah JMockit lebih baik daripada Mockito, melainkan bagaimana melakukannya di Mockito. Tetaplah membangun produk yang lebih baik daripada mencari peluang untuk menghancurkan kompetisi!
TheZuck
8
Poster asli hanya mengatakan bahwa dia menggunakan Mockito; hanya tersirat bahwa Mockito adalah persyaratan yang tetap dan sulit sehingga petunjuk bahwa JMockit dapat menangani situasi ini tidaklah tidak tepat.
Bombe
0

Jika Anda menginginkan alternatif untuk ReflectionTestUtils dari Spring di mockito, gunakan

Whitebox.setInternalState(first, "second", sec);
Szymon Zwoliński
sumber
Selamat Datang di Stack Overflow! Ada jawaban lain yang memberikan pertanyaan OP, dan mereka diposting bertahun-tahun yang lalu. Saat mengirim jawaban, pastikan Anda menambahkan solusi baru, atau penjelasan yang jauh lebih baik, terutama saat menjawab pertanyaan yang lebih lama atau mengomentari jawaban lain.
help-info.de