Bagaimana cara kerja Pencocok Mockito?

122

Matchers argumen Mockito (seperti any, argThat, eq, same, dan ArgumentCaptor.capture()) berperilaku sangat berbeda dari matchers Hamcrest.

  • Pencocokan mockito sering menyebabkan InvalidUseOfMatchersException, bahkan dalam kode yang dijalankan lama setelah pencocokan apa pun digunakan.

  • Pencocokan mockito terikat pada aturan aneh, seperti hanya mengharuskan penggunaan pencocokan Mockito untuk semua argumen jika satu argumen dalam metode tertentu menggunakan pencocokan.

  • Pencocok mockito dapat menyebabkan NullPointerException saat mengganti Answers atau saat menggunakan (Integer) any()dll.

  • Memfaktorkan ulang kode dengan pencocok Mockito dengan cara tertentu dapat menghasilkan pengecualian dan perilaku yang tidak terduga, dan mungkin gagal sepenuhnya.

Mengapa pencocokkan Mockito dirancang seperti ini, dan bagaimana penerapannya?

Jeff Bowman
sumber

Jawaban:

236

Pencocok mockito adalah metode statis dan panggilan ke metode tersebut, yang menggantikan argumen selama panggilan ke whendan verify.

Pencocokan Hamcrest (versi yang diarsipkan) (atau pencocokan gaya Hamcrest) adalah instance objek tujuan umum tanpa kewarganegaraan yang mengimplementasikan Matcher<T>dan mengekspos metode matches(T)yang mengembalikan nilai true jika objek cocok dengan kriteria Matcher. Mereka dimaksudkan agar bebas dari efek samping, dan umumnya digunakan dalam pernyataan seperti di bawah ini.

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));

Pencocokan mockito ada, terpisah dari pencocokan gaya Hamcrest, sehingga deskripsi ekspresi yang cocok langsung masuk ke dalam pemanggilan metode : Pencocokan mockito kembali di Tmana metode pencocok Hamcrest mengembalikan objek Matcher (jenis Matcher<T>).

Mockito matchers dipanggil melalui metode statis seperti eq, any, gt, dan startsWithdi org.mockito.Matchersdan org.mockito.AdditionalMatchers. Ada juga adaptor, yang telah berubah di seluruh versi Mockito:

  • Untuk Mockito 1.x, Matchersmenampilkan beberapa panggilan (seperti intThatatau argThat) adalah pencocok Mockito yang secara langsung menerima pencocok Hamcrest sebagai parameter. ArgumentMatcher<T>extended org.hamcrest.Matcher<T>, yang digunakan dalam representasi internal Hamcrest dan merupakan kelas dasar matcher Hamcrest alih-alih segala jenis Mockito matcher.
  • Untuk Mockito 2.0+, Mockito tidak lagi memiliki ketergantungan langsung pada Hamcrest. Matcherspanggilan yang diutarakan sebagai intThatatau argThatmembungkus ArgumentMatcher<T>objek yang tidak lagi diimplementasikan org.hamcrest.Matcher<T>tetapi digunakan dengan cara yang serupa. Adaptor Hamcrest seperti argThatdan intThatmasih tersedia, tetapi MockitoHamcrestsebagai gantinya telah dipindahkan .

Terlepas dari apakah matcher tersebut adalah Hamcrest atau hanya bergaya Hamcrest, mereka dapat diadaptasi seperti ini:

/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));

Dalam pernyataan di atas: foo.setPowerLeveladalah metode yang menerima file int. is(greaterThan(9000))mengembalikan a Matcher<Integer>, yang tidak akan berfungsi sebagai setPowerLevelargumen. intThatPencocok Mockito membungkus Pencocokan gaya-Hamcrest dan mengembalikan intsehingga dapat muncul sebagai argumen; Pencocok mockito ingin menggabungkan gt(9000)seluruh ekspresi itu menjadi satu panggilan, seperti pada baris pertama kode contoh.

Apa yang dilakukan / dikembalikan oleh pencocokkan

when(foo.quux(3, 5)).thenReturn(true);

Saat tidak menggunakan pencocok argumen, Mockito mencatat nilai argumen Anda dan membandingkannya dengan equalsmetodenya.

when(foo.quux(eq(3), eq(5))).thenReturn(true);    // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different

Saat Anda memanggil matcher like anyatau gt(lebih besar dari), Mockito menyimpan objek matcher yang menyebabkan Mockito melewati pemeriksaan kesetaraan itu dan menerapkan kecocokan pilihan Anda. Dalam kasus argumentCaptor.capture()itu menyimpan matcher yang menyimpan argumennya sebagai gantinya untuk pemeriksaan nanti.

Pencocokan mengembalikan nilai dummy seperti nol, koleksi kosong, atau null. Mockito mencoba mengembalikan nilai dummy yang sesuai dan aman, seperti 0 untuk anyInt()atau any(Integer.class)atau kosong List<String>untuk anyListOf(String.class). Karena penghapusan jenis, Mockito kekurangan informasi jenis untuk mengembalikan nilai apa pun kecuali nulluntuk any()atau argThat(...), yang dapat menyebabkan NullPointerException jika mencoba "membuka kotak otomatis" nullnilai primitif.

Pencocokan menyukai eqdan gtmengambil nilai parameter; idealnya, nilai-nilai ini harus dihitung sebelum stubbing / verifikasi dimulai. Memanggil tiruan di tengah-tengah mengejek panggilan lain dapat mengganggu penyumbatan.

Metode matcher tidak bisa digunakan sebagai nilai kembali; tidak ada cara untuk mengucapkan thenReturn(anyInt())atau thenReturn(any(Foo.class))dalam Mockito, misalnya. Mockito perlu tahu persis instance mana yang akan dikembalikan dalam panggilan stubbing, dan tidak akan memilih nilai pengembalian yang sewenang-wenang untuk Anda.

Detail implementasi

Pencocokan disimpan (sebagai pencocokkan objek gaya Hamcrest) dalam tumpukan yang terdapat dalam kelas yang disebut ArgumentMatcherStorage . MockitoCore dan Matcher masing-masing memiliki instance ThreadSafeMockingProgress , yang secara statis berisi instance MockingProgress yang menampung ThreadLocal. Ini ini MockingProgressImpl yang memegang beton ArgumentMatcherStorageImpl . Akibatnya, status tiruan dan pencocokan bersifat statis tetapi memiliki cakupan thread yang konsisten antara kelas Mockito dan Matchers.

Kebanyakan panggilan matcher hanya menambah tumpukan ini, dengan pengecualian untuk matchers seperti and, or, dannot . Ini sangat sesuai dengan (dan bergantung pada) urutan evaluasi Java , yang mengevaluasi argumen kiri-ke-kanan sebelum menggunakan metode:

when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6]      [5]  [1]       [4] [2]     [3]

Ini akan:

  1. Tambahkan anyInt()ke tumpukan.
  2. Tambahkan gt(10)ke tumpukan.
  3. Tambahkan lt(20)ke tumpukan.
  4. Hapus gt(10)dan lt(20)dan tambahkan and(gt(10), lt(20)).
  5. Panggilan foo.quux(0, 0), yang (kecuali jika dihentikan) mengembalikan nilai default false. Secara internal Mockito menandai quux(int, int)sebagai panggilan terbaru.
  6. Panggil when(false), yang membuang argumennya dan bersiap untuk metode stub yang quux(int, int)diidentifikasi di 5. Dua status valid hanya dengan panjang tumpukan 0 (persamaan) atau 2 (pencocokan), dan ada dua pencocokan di tumpukan (langkah 1 dan 4), jadi Mockito menghentikan metode dengan any()matcher untuk argumen pertama dan and(gt(10), lt(20))untuk argumen kedua dan membersihkan tumpukan.

Ini menunjukkan beberapa aturan:

  • Mockito tidak bisa membedakan antara quux(anyInt(), 0)dan quux(0, anyInt()). Mereka berdua terlihat seperti panggilan ke quux(0, 0)dengan satu pencocok int di tumpukan. Akibatnya, jika Anda menggunakan satu matcher, Anda harus mencocokkan semua argumen.

  • Urutan panggilan tidak hanya penting, itu yang membuat semua ini berfungsi . Mengekstrak matcher ke variabel umumnya tidak berfungsi, karena biasanya mengubah urutan panggilan. Namun, mengekstrak matcher ke metode berfungsi dengan baik.

    int between10And20 = and(gt(10), lt(20));
    /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
    // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().
    
    public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
    /* OK */  when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
    // The helper method calls the matcher methods in the right order.
  • Tumpukan tersebut cukup sering berubah sehingga Mockito tidak dapat mengawasi dengan sangat hati-hati. Itu hanya dapat memeriksa tumpukan saat Anda berinteraksi dengan Mockito atau tiruan, dan harus menerima pencocokan tanpa mengetahui apakah mereka digunakan segera atau ditinggalkan secara tidak sengaja. Secara teori, tumpukan harus selalu kosong di luar panggilan ke whenatau verify, tetapi Mockito tidak dapat memeriksanya secara otomatis. Anda dapat memeriksanya secara manual dengan Mockito.validateMockitoUsage().

  • Dalam panggilan ke when, Mockito sebenarnya memanggil metode yang dimaksud, yang akan memunculkan pengecualian jika Anda telah menghentikan metode untuk membuat pengecualian (atau memerlukan nilai bukan nol atau bukan nol). doReturndan doAnswer(dll) tidak menggunakan metode sebenarnya dan sering kali merupakan alternatif yang berguna.

  • Jika Anda telah disebut metode mock di tengah-tengah Stubbing (misalnya untuk menghitung jawaban untuk eqmatcher), Mockito akan memeriksa panjang tumpukan melawan bahwa panggilan sebagai gantinya, dan kemungkinan gagal.

  • Jika Anda mencoba melakukan sesuatu yang buruk, seperti menghentikan / memverifikasi metode terakhir , Mockito akan memanggil metode yang sebenarnya dan juga meninggalkan pencocok ekstra di tumpukan . The finalpemanggilan metode mungkin tidak membuang pengecualian, tetapi Anda mungkin mendapatkan InvalidUseOfMatchersException dari matchers liar ketika Anda berinteraksi berikutnya dengan pura-pura.

Masalah umum

  • InvalidUseOfMatchersException :

    • Periksa bahwa setiap argumen memiliki tepat satu pemanggil pencocokan, jika Anda menggunakan pencocokan sama sekali, dan bahwa Anda belum menggunakan pencocokan di luar panggilan whenatau verify. Pencocokan tidak boleh digunakan sebagai nilai kembali atau bidang / variabel yang dipotong.

    • Pastikan Anda tidak memanggil tiruan sebagai bagian dari memberikan argumen matcher.

    • Periksa apakah Anda tidak mencoba untuk menghentikan / memverifikasi metode terakhir dengan pencocok. Ini cara yang bagus untuk meninggalkan matcher di tumpukan, dan kecuali metode terakhir Anda mengeluarkan pengecualian, ini mungkin satu-satunya saat Anda menyadari bahwa metode yang Anda ejekan sudah final.

  • NullPointerException dengan argumen primitif: (Integer) any() mengembalikan null sedangkan any(Integer.class)mengembalikan 0; ini dapat menyebabkan NullPointerExceptionjika Anda mengharapkan, intbukan Integer. Bagaimanapun, prefer anyInt(), yang akan mengembalikan nol dan juga melewatkan langkah auto-boxing.

  • NullPointerException atau pengecualian lainnya: Panggilan ke when(foo.bar(any())).thenReturn(baz)akan benar-benar memanggil foo.bar(null) , yang mungkin telah Anda stub untuk membuat pengecualian saat menerima argumen null. Beralih untuk doReturn(baz).when(foo).bar(any()) melewati perilaku yang dihentikan .

Pemecahan masalah umum

  • Gunakan MockitoJUnitRunner , atau panggil metode atau validateMockitoUsageAnda secara eksplisit (yang akan dilakukan pelari untuk Anda secara otomatis). Ini akan membantu menentukan apakah Anda telah menyalahgunakan matcher.tearDown@After

  • Untuk tujuan debugging, tambahkan panggilan ke validateMockitoUsagedalam kode Anda secara langsung. Ini akan dibuang jika Anda memiliki sesuatu di tumpukan, yang merupakan peringatan yang baik untuk gejala yang buruk.

Jeff Bowman
sumber
2
Terima kasih untuk artikel ini. NullPointerException dengan format when / thenReturn menyebabkan masalah bagi saya, hingga saya mengubahnya menjadi doReturn / when.
yngwietiger
11

Hanya sedikit tambahan untuk jawaban luar biasa Jeff Bowman, karena saya menemukan pertanyaan ini saat mencari solusi untuk salah satu masalah saya sendiri:

Jika panggilan ke suatu metode cocok dengan lebih dari satu whenpanggilan terlatih tiruan , urutan whenpanggilan itu penting, dan harus dari yang paling luas ke paling spesifik. Mulai dari salah satu contoh Jeff:

when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);

adalah urutan yang memastikan hasil yang (mungkin) diinginkan:

foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false

Jika Anda membalik ketika panggilan maka hasilnya akan selalu true.

tibtof
sumber
2
Meskipun ini adalah informasi yang berguna, ini menganggap stubbing, bukan matchers , jadi mungkin tidak masuk akal untuk pertanyaan ini. Urutan memang penting, tetapi hanya rantai pencocokan yang ditentukan terakhir yang menang : Ini berarti stub yang ada bersama sering dinyatakan paling spesifik hingga yang paling kecil, tetapi dalam beberapa kasus Anda mungkin menginginkan penggantian yang sangat luas dari perilaku yang diolok-olok secara khusus dalam satu kasus pengujian , pada titik mana definisi yang luas mungkin perlu datang terakhir.
Jeff Bowman
1
@JeffBowman Saya pikir pertanyaan ini masuk akal karena pertanyaannya adalah tentang mockito matcher dan matcher dapat digunakan saat mematikan (seperti di sebagian besar contoh Anda). Sejak mencari google untuk penjelasan membawa saya ke pertanyaan ini, saya pikir ada gunanya memiliki informasi ini di sini.
mulai