Panduan Praktisi Pengujian Unit untuk Mockito Sehari-hari
Diterbitkan: 2022-03-11Pengujian unit telah menjadi wajib di era Agile, dan ada banyak alat yang tersedia untuk membantu pengujian otomatis. Salah satu alat tersebut adalah Mockito, kerangka kerja sumber terbuka yang memungkinkan Anda membuat dan mengonfigurasi objek tiruan untuk pengujian.
Dalam artikel ini, kita akan membahas membuat dan mengonfigurasi tiruan dan menggunakannya untuk memverifikasi perilaku yang diharapkan dari sistem yang sedang diuji. Kami juga akan sedikit menyelami internal Mockito untuk lebih memahami desain dan peringatannya. Kami akan menggunakan JUnit sebagai kerangka kerja pengujian unit, tetapi karena Mockito tidak terikat dengan JUnit, Anda dapat mengikutinya meskipun Anda menggunakan kerangka kerja yang berbeda.
Mendapatkan Mockito
Mendapatkan Mockito mudah hari ini. Jika Anda menggunakan Gradle, ini masalah menambahkan satu baris ini ke skrip build Anda:
testCompile "org.mockito:mockito−core:2.7.7"
Adapun mereka seperti saya yang masih lebih suka Maven, tambahkan saja Mockito ke dependensi Anda seperti:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>
Tentu saja, dunia jauh lebih luas daripada Maven dan Gradle. Anda bebas menggunakan alat manajemen proyek apa pun untuk mengambil artefak jar Mockito dari repositori pusat Maven.
Mendekati Mockito
Tes unit dirancang untuk menguji perilaku kelas atau metode tertentu tanpa bergantung pada perilaku dependensinya. Karena kita sedang menguji 'unit' kode terkecil, kita tidak perlu menggunakan implementasi sebenarnya dari dependensi ini. Selanjutnya, kami akan menggunakan implementasi yang sedikit berbeda dari dependensi ini saat menguji perilaku yang berbeda. Pendekatan tradisional yang terkenal untuk ini adalah membuat 'stub'– implementasi khusus dari antarmuka yang cocok untuk skenario tertentu. Implementasi tersebut biasanya memiliki logika hard-coded. Sebuah rintisan adalah semacam tes ganda. Jenis lain termasuk palsu, tiruan, mata-mata, boneka, dll.
Kami akan fokus hanya pada dua jenis tes ganda, 'mengolok-olok' dan 'mata-mata', karena ini banyak digunakan oleh Mockito.
mengolok-olok
Apa itu mengejek? Jelas, itu bukan tempat Anda mengolok-olok sesama pengembang. Mengejek untuk pengujian unit adalah ketika Anda membuat objek yang mengimplementasikan perilaku subsistem nyata dengan cara yang terkendali. Singkatnya, tiruan digunakan sebagai pengganti ketergantungan.
Dengan Mockito, Anda membuat tiruan, memberi tahu Mockito apa yang harus dilakukan ketika metode tertentu dipanggil, dan kemudian menggunakan instance tiruan dalam pengujian Anda alih-alih yang asli. Setelah pengujian, Anda dapat menanyakan tiruan untuk melihat metode spesifik apa yang dipanggil atau memeriksa efek samping dalam bentuk status yang diubah.
Secara default, Mockito menyediakan implementasi untuk setiap metode mock.
mata-mata
Mata-mata adalah jenis tes ganda lain yang dibuat oleh Mockito. Berbeda dengan ejekan, membuat mata-mata membutuhkan contoh untuk memata-matai. Secara default, mata-mata mendelegasikan semua panggilan metode ke objek nyata dan mencatat metode apa yang dipanggil dan dengan parameter apa. Itulah yang membuatnya menjadi mata-mata: Ini memata-matai objek nyata.
Pertimbangkan untuk menggunakan ejekan alih-alih mata-mata bila memungkinkan. Mata-mata mungkin berguna untuk menguji kode lama yang tidak dapat didesain ulang agar mudah diuji, tetapi kebutuhan untuk menggunakan mata-mata untuk mengejek sebagian kelas merupakan indikator bahwa kelas melakukan terlalu banyak, sehingga melanggar prinsip tanggung jawab tunggal.
Membangun Contoh Sederhana
Mari kita lihat demo sederhana di mana kita dapat menulis tes. Misalkan kita memiliki antarmuka UserRepository
dengan metode tunggal untuk menemukan pengguna dengan pengidentifikasinya. Kami juga memiliki konsep encoder kata sandi untuk mengubah kata sandi teks-jelas menjadi hash kata sandi. Baik UserRepository
dan PasswordEncoder
adalah dependensi (juga disebut kolaborator) dari UserService
yang disuntikkan melalui konstruktor. Berikut tampilan kode demo kami:
Repositori Pengguna
public interface UserRepository { User findById(String id); }
Pengguna
public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
PasswordEncoder
public interface PasswordEncoder { String encode(String password); }
Layanan Pengguna
public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }
Contoh kode ini dapat ditemukan di GitHub, sehingga Anda dapat mengunduhnya untuk ditinjau bersama artikel ini.
Menerapkan Mockito
Menggunakan kode contoh kita, mari kita lihat bagaimana menerapkan Mockito dan menulis beberapa tes.
Membuat Mock
Dengan Mockito, membuat tiruan semudah memanggil metode statis Mockito.mock()
:
import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
Perhatikan impor statis untuk Mockito. Untuk sisa artikel ini, kami secara implisit akan mempertimbangkan impor ini ditambahkan.
Setelah impor, kami mengejek PasswordEncoder
, sebuah antarmuka. Mockito tidak hanya mengolok-olok antarmuka tetapi juga kelas abstrak dan kelas non-final yang konkret. Di luar kotak, Mockito tidak dapat mengejek kelas akhir dan metode final atau statis, tetapi jika Anda benar-benar membutuhkannya, Mockito 2 menyediakan plugin MockMaker eksperimental.
Perhatikan juga bahwa metode equals()
dan hashCode()
tidak dapat diejek.
Membuat mata-mata
Untuk membuat mata-mata, Anda perlu memanggil metode statis Mockito spy()
dan memberikannya sebuah instance untuk memata-matai. Memanggil metode dari objek yang dikembalikan akan memanggil metode nyata kecuali metode tersebut di-stub. Panggilan-panggilan ini direkam dan fakta-fakta dari panggilan-panggilan ini dapat diverifikasi (lihat deskripsi lebih lanjut dari verify()
). Mari kita membuat mata-mata:
DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));
Membuat mata-mata tidak jauh berbeda dengan membuat tiruan. Selain itu, semua metode Mockito yang digunakan untuk mengonfigurasi tiruan juga berlaku untuk mengonfigurasi mata-mata.
Mata-mata jarang digunakan dibandingkan dengan tiruan, tetapi Anda mungkin menganggapnya berguna untuk menguji kode lama yang tidak dapat difaktorkan ulang, di mana pengujiannya memerlukan tiruan sebagian. Dalam kasus tersebut, Anda bisa membuat mata-mata dan mematikan beberapa metodenya untuk mendapatkan perilaku yang Anda inginkan.
Nilai Pengembalian Default
Memanggil mock(PasswordEncoder.class)
mengembalikan sebuah instance dari PasswordEncoder
. Kami bahkan dapat memanggil metodenya, tetapi apa yang akan mereka kembalikan? Secara default, semua metode tiruan mengembalikan nilai "tidak diinisialisasi" atau "kosong", misalnya, nol untuk tipe numerik (baik primitif maupun kotak), false untuk boolean, dan nol untuk sebagian besar tipe lainnya.
Pertimbangkan antarmuka berikut:
interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }
Sekarang perhatikan cuplikan berikut, yang memberikan gambaran tentang nilai default yang diharapkan dari metode tiruan:
Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());
Metode Menghentikan
Ejekan yang segar dan tidak diubah hanya berguna dalam kasus yang jarang terjadi. Biasanya, kami ingin mengonfigurasi tiruan dan menentukan apa yang harus dilakukan ketika metode tertentu dari tiruan dipanggil. Ini disebut stubing .
Mockito menawarkan dua cara mematikan. Cara pertama adalah “ ketika metode ini dipanggil, maka lakukan sesuatu.” Perhatikan cuplikan berikut:
when(passwordEncoder.encode("1")).thenReturn("a");
Bunyinya hampir seperti bahasa Inggris: “Ketika passwordEncoder.encode(“1”)
dipanggil, kembalikan a
.”
Cara mematikan kedua lebih seperti "Lakukan sesuatu ketika metode tiruan ini dipanggil dengan argumen berikut." Cara mematikan ini lebih sulit dibaca karena penyebabnya ditentukan di bagian akhir. Mempertimbangkan:
doReturn("a").when(passwordEncoder).encode("1");
Cuplikan dengan metode penghentian ini akan berbunyi: “Kembalikan a
ketika metode encode()
passwordEncoder
dipanggil dengan argumen 1
.”
Cara pertama dianggap lebih disukai karena typesafe dan karena lebih mudah dibaca. Namun, jarang, Anda terpaksa menggunakan cara kedua, seperti saat mematikan metode mata-mata yang sebenarnya karena memanggilnya mungkin memiliki efek samping yang tidak diinginkan.
Mari kita jelajahi secara singkat metode stubbing yang disediakan oleh Mockito. Kami akan menyertakan kedua cara mematikan dalam contoh kami.
Mengembalikan Nilai
thenReturn
atau doReturn()
digunakan untuk menentukan nilai yang akan dikembalikan pada pemanggilan metode.
//”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");
atau
//”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");
Anda juga dapat menentukan beberapa nilai yang akan dikembalikan sebagai hasil dari pemanggilan metode yang berurutan. Nilai terakhir akan digunakan sebagai hasil untuk semua pemanggilan metode lebih lanjut.
//when when(passwordEncoder.encode("1")).thenReturn("a", "b");
atau
//do doReturn("a", "b").when(passwordEncoder).encode("1");
Hal yang sama dapat dicapai dengan cuplikan berikut:
when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");
Pola ini juga dapat digunakan dengan metode stubbing lainnya untuk menentukan hasil panggilan berurutan.
Mengembalikan Tanggapan Kustom
then()
, alias untuk thenAnswer()
, dan doAnswer()
mencapai hal yang sama, yaitu menyiapkan jawaban khusus untuk dikembalikan saat metode dipanggil, seperti:
when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");
atau
doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");
Satu-satunya argumen yang thenAnswer()
adalah implementasi dari antarmuka Answer
. Ini memiliki metode tunggal dengan parameter tipe InvocationOnMock
.
Anda juga dapat melempar pengecualian sebagai hasil dari pemanggilan metode:
when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });
…atau panggil metode sebenarnya dari suatu kelas (tidak berlaku untuk antarmuka):
Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());
Anda benar jika Anda berpikir bahwa itu terlihat tidak praktis. Mockito menyediakan thenCallRealMethod()
dan thenThrow()
untuk merampingkan aspek pengujian Anda ini.
Memanggil Metode Nyata
Seperti namanya, thenCallRealMethod()
dan doCallRealMethod()
memanggil metode sebenarnya pada objek tiruan:
Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());
Memanggil metode nyata mungkin berguna pada tiruan parsial, tetapi pastikan bahwa metode yang dipanggil tidak memiliki efek samping yang tidak diinginkan dan tidak bergantung pada status objek. Jika ya, mata-mata mungkin lebih cocok daripada tiruan.
Jika Anda membuat tiruan antarmuka dan mencoba mengonfigurasi rintisan untuk memanggil metode nyata, Mockito akan memberikan pengecualian dengan pesan yang sangat informatif. Perhatikan cuplikan berikut:
when(passwordEncoder.encode("1")).thenCallRealMethod();
Mockito akan gagal dengan pesan berikut:
Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
Kudos kepada pengembang Mockito karena cukup peduli untuk memberikan deskripsi yang menyeluruh!
Melempar Pengecualian
thenThrow()
dan doThrow()
mengonfigurasi metode tiruan untuk melempar pengecualian:
when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());
atau
doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");
Mockito memastikan bahwa pengecualian yang dilemparkan valid untuk metode stubbed tertentu dan akan mengeluh jika pengecualian tidak ada dalam daftar pengecualian metode yang diperiksa. Pertimbangkan hal berikut:
when(passwordEncoder.encode("1")).thenThrow(new IOException());
Ini akan menyebabkan kesalahan:
org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException
Seperti yang Anda lihat, Mockito mendeteksi bahwa encode()
tidak dapat melempar IOException
.
Anda juga dapat melewati kelas pengecualian alih-alih meneruskan instance pengecualian:
when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);
atau
doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");
Karena itu, Mockito tidak dapat memvalidasi kelas pengecualian dengan cara yang sama seperti memvalidasi instance pengecualian, jadi Anda harus disiplin dan tidak melewatkan objek kelas ilegal. Misalnya, berikut ini akan melempar IOException
meskipun encode()
tidak diharapkan untuk membuang pengecualian yang dicentang:
when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");
Antarmuka Mengejek dengan Metode Default
Perlu dicatat bahwa saat membuat tiruan untuk antarmuka, Mockito mengolok-olok semua metode antarmuka itu. Sejak Java 8, antarmuka mungkin berisi metode default bersama dengan metode abstrak. Metode ini juga diejek, jadi Anda harus berhati-hati untuk menjadikannya sebagai metode default.
Perhatikan contoh berikut:
interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());
Dalam contoh ini, assertFalse()
akan berhasil. Jika bukan itu yang Anda harapkan, pastikan Anda telah membuat Mockito memanggil metode sebenarnya, seperti:
AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());
Pencocokan Argumen
Di bagian sebelumnya, kami mengonfigurasi metode tiruan kami dengan nilai yang tepat sebagai argumen. Dalam kasus tersebut, Mockito hanya memanggil equals()
secara internal untuk memeriksa apakah nilai yang diharapkan sama dengan nilai sebenarnya.
Namun, terkadang kita tidak mengetahui nilai-nilai ini sebelumnya.
Mungkin kita hanya tidak peduli dengan nilai aktual yang disampaikan sebagai argumen, atau mungkin kita ingin mendefinisikan reaksi untuk rentang nilai yang lebih luas. Semua skenario ini (dan lebih banyak lagi) dapat diatasi dengan pencocokan argumen. Idenya sederhana: Alih-alih memberikan nilai yang tepat, Anda memberikan pencocokan argumen untuk Mockito untuk mencocokkan argumen metode.
Perhatikan cuplikan berikut:
when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));
Anda dapat melihat bahwa hasilnya sama tidak peduli berapa nilai yang kami berikan ke encode()
karena kami menggunakan pencocokan argumen anyString()
di baris pertama itu. Jika kita menulis ulang baris itu dalam bahasa Inggris biasa, itu akan terdengar seperti "ketika pembuat sandi diminta untuk menyandikan string apa pun, lalu kembalikan string 'tepat.'"
Mockito mengharuskan Anda untuk memberikan semua argumen baik dengan pencocokan atau dengan nilai yang tepat. Jadi, jika suatu metode memiliki lebih dari satu argumen dan Anda ingin menggunakan pencocokan argumen hanya untuk beberapa argumennya, lupakan saja. Anda tidak dapat menulis kode seperti ini:
abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);
Untuk memperbaiki kesalahan, kita harus mengganti baris terakhir untuk menyertakan pencocokan argumen eq
untuk a
, sebagai berikut:
when(mock.call(eq("a"), anyInt())).thenReturn(true);
Di sini kita telah menggunakan pencocokan argumen eq()
dan anyInt()
, tetapi ada banyak lagi yang tersedia. Untuk daftar lengkap pencocokan argumen, lihat dokumentasi di kelas org.mockito.ArgumentMatchers
.
Penting untuk dicatat bahwa Anda tidak dapat menggunakan pencocokan argumen di luar verifikasi atau penghentian. Misalnya, Anda tidak dapat memiliki yang berikut ini:
//this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);
Mockito akan mendeteksi pencocokan argumen yang salah tempat dan melempar InvalidUseOfMatchersException
. Verifikasi dengan pencocokan argumen harus dilakukan dengan cara ini:
verify(mock).encode(or(eq("a"), endsWith("b")));
Pencocokan argumen juga tidak dapat digunakan sebagai nilai balik. Mockito tidak dapat mengembalikan anyString()
atau apa pun; nilai yang tepat diperlukan saat mematikan panggilan.
Pencocokan Khusus
Pencocokan khusus datang untuk menyelamatkan ketika Anda perlu memberikan beberapa logika pencocokan yang belum tersedia di Mockito. Keputusan untuk membuat pencocokan khusus tidak boleh dianggap enteng karena kebutuhan untuk mencocokkan argumen dengan cara yang tidak sepele menunjukkan masalah dalam desain atau bahwa pengujian menjadi terlalu rumit.
Karena itu, ada baiknya memeriksa apakah Anda dapat menyederhanakan pengujian dengan menggunakan beberapa pencocokan argumen yang lunak seperti isNull()
dan nullable()
sebelum menulis pencocokan khusus. Jika Anda masih merasa perlu untuk menulis pencocok argumen, Mockito menyediakan serangkaian metode untuk melakukannya.
Perhatikan contoh berikut:
FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));
Di sini kita membuat matcher argumen hasLuck
dan menggunakan argThat()
untuk meneruskan matcher sebagai argumen ke metode mocked, mematikannya untuk mengembalikan true
jika nama file diakhiri dengan "luck". Anda dapat memperlakukan ArgumentMatcher
sebagai antarmuka fungsional dan membuat instance-nya dengan lambda (yang telah kami lakukan dalam contoh). Sintaks yang kurang ringkas akan terlihat seperti:
ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };
Jika Anda perlu membuat pencocokan argumen yang berfungsi dengan tipe primitif, ada beberapa metode lain untuk itu di org.mockito.ArgumentMatchers
:
- charItu(PencocokanArgumentMatcher<Karakter>)
- booleanThat(PencocokanArgumentMatcher<Boolean>)
- byteItu(PencocokanArgumentMatcher<Byte>)
- shortItu(PencocokanArgumentMatcher<Pendek>)
- intItu(PencocokanArgumentMatcher<Integer>)
- longThat(PencocokanArgumentMatcher<Long>)
- floatThat(PencocokanArgumentMatcher<Float>)
- doubleThat(PencocokanArgumentMatcher<Double>)
Menggabungkan Matcher
Tidak selalu layak untuk membuat pencocokan argumen khusus saat kondisi terlalu rumit untuk ditangani dengan pencocokan dasar; terkadang menggabungkan korek api akan berhasil. Mockito menyediakan pencocokan argumen untuk mengimplementasikan operasi logika umum ('tidak', 'dan', 'atau') pada pencocokan argumen yang cocok dengan tipe primitif dan non-primitif. Matcher ini diimplementasikan sebagai metode statis di kelas org.mockito.AdditionalMatchers
.
Perhatikan contoh berikut:
when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));
Di sini kami telah menggabungkan hasil dari dua pencocokan argumen: eq("1")
dan contains("a")
. Ekspresi terakhir, or(eq("1"), contains("a"))
, dapat ditafsirkan sebagai "string argumen harus sama dengan "1" atau berisi "a".
Perhatikan bahwa ada matcher yang kurang umum terdaftar di kelas org.mockito.AdditionalMatchers
, seperti geq()
, leq()
, gt()
dan lt()
, yang merupakan perbandingan nilai yang berlaku untuk nilai primitif dan instance java.lang.Comparable
.

Memverifikasi Perilaku
Setelah tiruan atau mata-mata telah digunakan, kami dapat verify
bahwa interaksi tertentu telah terjadi. Secara harfiah, kami mengatakan "Hei, Mockito, pastikan metode ini dipanggil dengan argumen ini."
Perhatikan contoh buatan berikut:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");
Di sini kita telah menyiapkan tiruan dan memanggil metode encode()
-nya. Baris terakhir memverifikasi bahwa metode encode()
tiruan dipanggil dengan nilai argumen spesifik a
. Harap dicatat bahwa memverifikasi doa yang di-stub adalah berlebihan; tujuan dari cuplikan sebelumnya adalah untuk menunjukkan ide melakukan verifikasi setelah beberapa interaksi terjadi.
Jika kita mengubah baris terakhir untuk memiliki argumen yang berbeda—katakanlah, b
—tes sebelumnya akan gagal dan Mockito akan mengeluh bahwa pemanggilan yang sebenarnya memiliki argumen yang berbeda ( b
bukannya a
yang diharapkan).
Pencocokan argumen dapat digunakan untuk verifikasi seperti untuk mematikan:
verify(passwordEncoder).encode(anyString());
Secara default, Mockito memverifikasi bahwa metode ini dipanggil sekali, tetapi Anda dapat memverifikasi sejumlah pemanggilan:
// verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());
Fitur verify()
yang jarang digunakan adalah kemampuannya untuk gagal pada batas waktu, yang terutama berguna untuk menguji kode bersamaan. Misalnya, jika pembuat sandi kata sandi kita dipanggil di utas lain secara bersamaan dengan verify()
, kita dapat menulis tes sebagai berikut:
usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");
Tes ini akan berhasil jika encode()
dipanggil dan selesai dalam waktu 500 milidetik atau kurang. Jika Anda perlu menunggu periode penuh yang Anda tentukan, gunakan after()
alih-alih timeout()
:
verify(passwordEncoder, after(500)).encode("a");
Mode verifikasi lainnya ( times()
, atLeast()
, dll) dapat digabungkan dengan timeout()
dan after()
untuk membuat pengujian yang lebih rumit:
// passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");
Selain times()
, mode verifikasi yang didukung termasuk only()
, atLeast()
dan atLeastOnce()
(sebagai alias untuk atLeast(1)
).
Mockito juga memungkinkan Anda untuk memverifikasi urutan panggilan dalam sekelompok ejekan. Ini bukan fitur yang sering digunakan, tetapi mungkin berguna jika urutan pemanggilan penting. Perhatikan contoh berikut:
PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");
Jika kami mengatur ulang urutan panggilan simulasi, pengujian akan gagal dengan VerificationInOrderFailure
.
Tidak adanya pemanggilan juga dapat diverifikasi menggunakan verifyZeroInteractions()
. Metode ini menerima tiruan atau ejekan sebagai argumen dan akan gagal jika ada metode dari tiruan yang diteruskan dipanggil.
Perlu juga disebutkan metode verifyNoMoreInteractions()
, karena menggunakan ejekan sebagai argumen dan dapat digunakan untuk memeriksa bahwa setiap panggilan pada ejekan itu telah diverifikasi.
Menangkap Argumen
Selain memverifikasi bahwa suatu metode dipanggil dengan argumen tertentu, Mockito memungkinkan Anda untuk menangkap argumen tersebut sehingga nanti Anda dapat menjalankan pernyataan khusus pada mereka. Dengan kata lain, Anda mengatakan "Hei, Mockito, verifikasi bahwa metode ini dipanggil, dan beri saya nilai argumen yang digunakannya."
Mari kita buat tiruan dari PasswordEncoder
, panggil encode()
, tangkap argumennya, dan periksa nilainya:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());
Seperti yang Anda lihat, kami meneruskan passwordCaptor.capture()
sebagai argumen encode()
untuk verifikasi; ini secara internal membuat pencocokan argumen yang menyimpan argumen. Kemudian kami mengambil nilai yang ditangkap dengan passwordCaptor.getValue()
dan memeriksanya dengan assertEquals()
.
Jika kita perlu menangkap argumen di beberapa panggilan, ArgumentCaptor
memungkinkan Anda mengambil semua nilai dengan getAllValues()
, seperti:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());
Teknik yang sama dapat digunakan untuk menangkap argumen metode arity variabel (juga dikenal sebagai varargs).
Menguji Contoh Sederhana Kami
Sekarang kita tahu lebih banyak tentang Mockito, saatnya untuk kembali ke demo kita. Mari kita tulis pengujian metode isValidUser
. Inilah yang mungkin terlihat seperti:
public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }
Menyelam di bawah API
Mockito menyediakan API yang mudah dibaca dan nyaman, tetapi mari kita jelajahi beberapa cara kerja internalnya untuk memahami keterbatasannya dan menghindari kesalahan aneh.
Mari kita periksa apa yang terjadi di dalam Mockito saat cuplikan berikut dijalankan:
// 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));
Jelas, baris pertama menciptakan tiruan. Mockito menggunakan ByteBuddy untuk membuat subkelas dari kelas yang diberikan. Objek kelas baru memiliki nama yang dihasilkan seperti demo.mockito.PasswordEncoder$MockitoMock$1953422997
, equals()
-nya akan bertindak sebagai pengecekan identitas, dan hashCode()
akan mengembalikan kode hash identitas. Setelah kelas dibuat dan dimuat, instance-nya dibuat menggunakan Objenesis.
Mari kita lihat baris berikutnya:
when(mock.encode("a")).thenReturn("1");
Urutannya penting: Pernyataan pertama yang dieksekusi di sini adalah mock.encode("a")
, yang akan memanggil encode()
pada tiruan dengan nilai pengembalian default null
. Jadi sungguh, kami melewatkan null
sebagai argumen when()
. Mockito tidak peduli nilai pasti apa yang diteruskan ke when()
karena ia menyimpan informasi tentang pemanggilan metode yang diolok-olok dalam apa yang disebut 'penghentian yang sedang berlangsung' ketika metode itu dipanggil. Kemudian, saat kita memanggil when()
, Mockito menarik objek stubbing yang sedang berlangsung dan mengembalikannya sebagai hasil dari when()
. Kemudian kita memanggil thenReturn(“1”)
pada objek stubbing yang sedang berlangsung yang dikembalikan.
Baris ketiga, mock.encode("a");
sederhana: Kami memanggil metode stub. Secara internal, Mockito menyimpan pemanggilan ini untuk verifikasi lebih lanjut dan mengembalikan jawaban pemanggilan yang di-stub; dalam kasus kami, ini adalah string 1
.
Di baris keempat ( verify(mock).encode(or(eq("a"), endsWith("b")));
), kami meminta Mockito untuk memverifikasi bahwa ada pemanggilan encode()
dengan itu argumen tertentu.
verify()
dijalankan terlebih dahulu, yang mengubah status internal Mockito menjadi mode verifikasi. Penting untuk dipahami bahwa Mockito mempertahankan statusnya dalam ThreadLocal
. Ini memungkinkan untuk mengimplementasikan sintaks yang bagus tetapi, di sisi lain, ini dapat menyebabkan perilaku aneh jika kerangka kerja digunakan secara tidak benar (jika Anda mencoba menggunakan pencocokan argumen di luar verifikasi atau penghentian, misalnya).
Jadi bagaimana Mockito membuat or
matcher? Pertama, eq("a")
dipanggil, dan matcher yang equals
ditambahkan ke tumpukan matcher. Kedua, endsWith("b")
dipanggil, dan pencocokan endsWith
ditambahkan ke tumpukan. Akhirnya, or(null, null)
dipanggil—menggunakan dua matcher yang muncul dari stack, membuat or
matcher, dan mendorongnya ke stack. Akhirnya, encode()
dipanggil. Mockito kemudian memverifikasi bahwa metode tersebut dipanggil sebanyak yang diharapkan dan dengan argumen yang diharapkan.
Sementara pencocokan argumen tidak dapat diekstraksi ke variabel (karena mengubah urutan panggilan), mereka dapat diekstraksi ke metode. Ini mempertahankan urutan panggilan dan menjaga tumpukan dalam keadaan yang benar:
verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }
Mengubah Jawaban Default
Di bagian sebelumnya, kami membuat tiruan kami sedemikian rupa sehingga ketika metode tiruan apa pun dipanggil, mereka mengembalikan nilai "kosong". Perilaku ini dapat dikonfigurasi. Anda bahkan dapat memberikan implementasi org.mockito.stubbing.Answer
Anda sendiri jika yang disediakan oleh Mockito tidak cocok, tetapi ini mungkin merupakan indikasi bahwa ada sesuatu yang salah ketika pengujian unit menjadi terlalu rumit. Ingat prinsip KISS!
Mari kita jelajahi penawaran Mockito tentang jawaban default yang telah ditentukan sebelumnya:
RETURNS_DEFAULTS
adalah strategi default; itu tidak layak disebutkan secara eksplisit saat menyiapkan tiruan.CALLS_REAL_METHODS
membuat pemanggilan unstubbed memanggil metode nyata.RETURNS_SMART_NULLS
menghindariNullPointerException
dengan mengembalikanSmartNull
alih-alihnull
saat menggunakan objek yang dikembalikan oleh pemanggilan metode yang tidak di-stub. Anda masih akan gagal denganNullPointerException
, tetapiSmartNull
memberi Anda pelacakan tumpukan yang lebih baik dengan baris tempat metode unstubbed dipanggil. Ini membuatnya berharga untukRETURNS_SMART_NULLS
menjadi jawaban default di Mockito!RETURNS_MOCKS
pertama-tama mencoba mengembalikan nilai "kosong" biasa, lalu mengolok-olok, jika memungkinkan, dannull
sebaliknya. Kriteria kekosongan sedikit berbeda dari apa yang telah kita lihat sebelumnya: Alih-alih mengembalikannull
untuk string dan array, tiruan yang dibuat denganRETURNS_MOCKS
masing-masing mengembalikan string kosong dan array kosong.RETURNS_SELF
berguna untuk mengejek pembangun. Dengan pengaturan ini, tiruan akan mengembalikan instance dirinya sendiri jika suatu metode dipanggil yang mengembalikan sesuatu dengan tipe yang sama dengan kelas (atau superclass) dari kelas tiruan.RETURNS_DEEP_STUBS
lebih dalam dariRETURNS_MOCKS
dan membuat tiruan yang dapat mengembalikan tiruan dari tiruan dari tiruan, dll. Berbeda denganRETURNS_MOCKS
, aturan kekosongan adalah default diRETURNS_DEEP_STUBS
, sehingga mengembalikannull
untuk string dan array:
interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());
Menamai Mock
Mockito memungkinkan Anda memberi nama tiruan, fitur yang berguna jika Anda memiliki banyak tiruan dalam pengujian dan perlu membedakannya. Yang mengatakan, perlu untuk mengolok-olok nama mungkin merupakan gejala dari desain yang buruk. Pertimbangkan hal berikut:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
Mockito akan mengeluh, tetapi karena kami belum secara resmi menamai tiruan itu, kami tidak tahu yang mana:
Wanted but not invoked: passwordEncoder.encode(<any string>);
Beri nama mereka dengan memasukkan string pada konstruksi:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());
Sekarang pesan kesalahan lebih ramah dan jelas menunjuk ke robustPasswordEncoder
:
Wanted but not invoked: robustPasswordEncoder.encode(<any string>);
Implementing Multiple Mock Interfaces
Sometimes, you may wish to create a mock that implements several interfaces. Mockito is able to do that easily, like so:
PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);
Listening Invocations
A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.
InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");
In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger
(don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging()
setting:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());
Take notice, though, that Mockito will call the listeners even when you're stubbing methods. Perhatikan contoh berikut:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");
This snippet will produce an output similar to the following:
############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"
Note that the first logged invocation corresponds to calling encode()
while stubbing it. It's the next invocation that corresponds to calling the stubbed method.
Pengaturan lainnya
Mockito offers a few more settings that let you do the following:
- Enable mock serialization by using
withSettings().serializable()
. - Turn off recording of method invocations to save memory (this will make verification impossible) by using
withSettings().stubOnly()
. - Use the constructor of a mock when creating its instance by using
withSettings().useConstructor()
. When mocking inner non-static classes, add anouterInstance()
setting, like so:withSettings().useConstructor().outerInstance(outerObject)
.
If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance()
setting, so that Mockito will create a spy on the instance you provide, like so:
UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));
When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.
Note that, when you create a spy, you're basically creating a mock that calls real methods:
// creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));
When Mockito Tastes Bad
It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.
Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.
Clearing Invocations
Mockito allows for clearing invocations for mocks while preserving stubbing, like so:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);
Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.
Resetting a Mock
Resetting a mock with reset()
is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.
Overusing Verify
Another bad habit is trying to replace every assert with Mockito's verify()
. It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify()
, while confirming the observable results of an executed action is done with asserts.
Mockito Is about Frame of Mind
Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.
With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.