Mockito vs EasyMock vs JMockit

1. Perkenalan

1.1. Gambaran

Dalam posting ini, kita akan berbicara tentang mocking : apa itu, mengapa menggunakannya, dan beberapa contoh bagaimana memalsukan test case yang sama menggunakan beberapa pustaka mocking yang paling banyak digunakan untuk Java.

Kita akan mulai dengan beberapa definisi formal / semi formal dari konsep mengejek; lalu kami akan menyajikan kasus yang sedang diuji, menindaklanjuti dengan contoh untuk setiap perpustakaan dan berakhir dengan beberapa kesimpulan. Perpustakaan yang dipilih adalah Mockito, EasyMock, dan JMockit.

Jika Anda merasa sudah mengetahui dasar-dasar ejekan, mungkin Anda bisa langsung ke Poin 2 tanpa membaca tiga poin berikutnya.

1.2. Alasan Menggunakan Ejekan

Kami akan mulai mengasumsikan bahwa Anda sudah membuat kode mengikuti beberapa metodologi pengembangan yang didorong yang berpusat pada tes (TDD, ATDD atau BDD). Atau Anda ingin membuat pengujian untuk kelas yang sudah ada yang bergantung pada dependensi untuk mencapai fungsinya.

Bagaimanapun, saat unit-testing sebuah kelas, kita hanya ingin menguji fungsionalitasnya dan bukan dependensinya (baik karena kita mempercayai implementasinya atau karena kita akan mengujinya sendiri).

Untuk mencapai ini, kita perlu menyediakan objek yang sedang diuji, pengganti yang dapat kita kontrol untuk ketergantungan tersebut. Dengan cara ini kita bisa memaksakan nilai pengembalian yang ekstrim, pengecualian melempar atau hanya mengurangi metode yang memakan waktu ke nilai pengembalian tetap.

Penggantian terkontrol ini hanyalah tiruan , dan ini akan membantu Anda menyederhanakan pengkodean pengujian dan mengurangi waktu eksekusi pengujian.

1.3. Konsep dan Definisi Mock

Mari kita lihat empat definisi dari artikel yang ditulis oleh Martin Fowler yang merangkum dasar-dasar yang harus diketahui semua orang tentang ejekan:

  • Objek dummy diedarkan tetapi tidak pernah benar-benar digunakan. Biasanya, mereka hanya digunakan untuk mengisi daftar parameter.
  • Objek palsu memiliki implementasi yang berfungsi, tetapi biasanya, mengambil beberapa pintasan yang membuatnya tidak cocok untuk produksi (database dalam memori adalah contoh yang baik).
  • Stubs memberikan jawaban terekam untuk panggilan yang dilakukan selama pengujian, biasanya tidak menanggapi sama sekali apa pun di luar program yang telah diprogram untuk pengujian tersebut. Stub juga dapat merekam informasi tentang panggilan, seperti rintisan gateway email yang mengingat pesan yang 'dikirim', atau mungkin hanya berapa banyak pesan yang 'dikirim'.
  • Ejekan adalah apa yang kita bicarakan di sini: objek yang telah diprogram sebelumnya dengan ekspektasi yang membentuk spesifikasi panggilan yang diharapkan diterima.

1.4 Untuk Mengolok-olok atau Tidak Mengolok: Itulah Pertanyaannya

Tidak semuanya harus diolok-olok . Terkadang lebih baik melakukan uji integrasi karena mengejek metode / fitur tersebut hanya akan berfungsi untuk keuntungan kecil yang sebenarnya. Dalam kasus pengujian kami (yang akan ditampilkan di poin berikutnya) yang akan menguji LoginDao .

The LoginDao akan menggunakan beberapa pihak ketiga perpustakaan untuk akses DB, dan mengejek itu hanya akan terdiri atas meyakinkan bahwa parameter telah disiapkan untuk panggilan, tapi kami masih perlu uji bahwa panggilan mengembalikan data yang kita inginkan.

Oleh karena itu, ini tidak akan disertakan dalam contoh ini (meskipun kita bisa menulis pengujian unit dengan panggilan tiruan untuk panggilan perpustakaan pihak ketiga DAN pengujian integrasi dengan DBUnit untuk menguji kinerja sebenarnya dari perpustakaan pihak ketiga).

2. Kasus Uji

Dengan semua yang ada di bagian sebelumnya dalam pikiran, mari kita usulkan kasus uji yang cukup umum dan bagaimana kita akan mengujinya menggunakan tiruan (bila masuk akal untuk menggunakan tiruan). Ini akan membantu kami memiliki skenario umum untuk nanti dapat membandingkan pustaka tiruan yang berbeda.

2.1 Usulan Kasus

Test case yang diajukan akan menjadi proses login pada aplikasi dengan arsitektur berlapis.

Permintaan login akan ditangani oleh pengontrol, yang menggunakan layanan, yang menggunakan DAO (yang mencari kredensial pengguna di DB). Kami tidak akan terlalu memperdalam implementasi setiap lapisan dan akan lebih fokus pada interaksi antara komponen setiap lapisan.

Dengan cara ini, kita akan memiliki LoginController , LoginService, dan LoginDAO . Mari kita lihat diagram untuk klarifikasi:

2.2 Implementasi

Kami akan mengikuti sekarang dengan implementasi yang digunakan untuk kasus pengujian, sehingga kami dapat memahami apa yang terjadi (atau apa yang seharusnya terjadi) pada pengujian.

Kami akan mulai dengan model yang digunakan untuk semua operasi, UserForm , yang hanya akan menampung nama pengguna dan kata sandi (kami menggunakan pengubah akses publik untuk menyederhanakan) dan metode pengambil untuk bidang nama pengguna untuk memungkinkan pemalsuan untuk properti itu:

public class UserForm { public String password; public String username; public String getUsername(){ return username; } }

Mari kita ikuti dengan LoginDAO , yang akan membatalkan fungsionalitas karena kami hanya ingin metodenya ada di sana sehingga kami dapat mengejeknya saat diperlukan:

public class LoginDao { public int login(UserForm userForm){ return 0; } }

LoginDao akan digunakan oleh LoginService dalam metode loginnya . LoginService juga akan memiliki metode setCurrentUser yang mengembalikan void untuk menguji tiruan tersebut.

public class LoginService { private LoginDao loginDao; private String currentUser; public boolean login(UserForm userForm) { assert null != userForm; int loginResults = loginDao.login(userForm); switch (loginResults){ case 1: return true; default: return false; } } public void setCurrentUser(String username) { if(null != username){ this.currentUser = username; } } }

Terakhir, LoginController akan menggunakan LoginService untuk metode loginnya . Ini akan mencakup:

  • kasus di mana tidak ada panggilan ke layanan tiruan yang akan dilakukan.
  • kasus di mana hanya satu metode yang akan dipanggil.
  • kasus di mana semua metode akan dipanggil.
  • kasus di mana pengecualian melempar akan diuji.
public class LoginController { public LoginService loginService; public String login(UserForm userForm){ if(null == userForm){ return "ERROR"; }else{ boolean logged; try { logged = loginService.login(userForm); } catch (Exception e) { return "ERROR"; } if(logged){ loginService.setCurrentUser(userForm.getUsername()); return "OK"; }else{ return "KO"; } } } }

Sekarang setelah kita melihat apa yang kita coba uji, mari kita lihat bagaimana kita akan mengejeknya dengan setiap perpustakaan.

3. Uji Pengaturan

3.1 Mockito

Untuk Mockito kami akan menggunakan versi 2.8.9.

Cara termudah untuk membuat dan menggunakan tiruan adalah melalui anotasi @Mock dan @InjectMocks . Yang pertama akan membuat tiruan untuk kelas yang digunakan untuk mendefinisikan bidang dan yang kedua akan mencoba untuk memasukkan tiruan yang dibuat tersebut ke dalam tiruan beranotasi.

Ada lebih banyak anotasi seperti @Spy yang memungkinkan Anda membuat tiruan sebagian (tiruan yang menggunakan implementasi normal dalam metode non-tiruan).

Karena itu, Anda perlu memanggil MockitoAnnotations.initMocks (this) sebelum menjalankan pengujian apa pun yang akan menggunakan ejekan tersebut agar semua "keajaiban" ini berfungsi. Ini biasanya dilakukan dengan metode beranotasi @Before . Anda juga dapat menggunakan MockitoJUnitRunner .

public class LoginControllerTest { @Mock private LoginDao loginDao; @Spy @InjectMocks private LoginService spiedLoginService; @Mock private LoginService loginService; @InjectMocks private LoginController loginController; @Before public void setUp() { loginController = new LoginController(); MockitoAnnotations.initMocks(this); } }

3.2 EasyMock

Untuk EasyMock, kami akan menggunakan versi 3.4 (Javadoc). Perhatikan bahwa dengan EasyMock, agar tiruan mulai "bekerja", Anda harus memanggil EasyMock.replay (tiruan) pada setiap metode pengujian, atau Anda akan menerima pengecualian.

Mocks and tested classes can also be defined via annotations, but in this case, instead of calling a static method for it to work, we'll be using the EasyMockRunner for the test class.

Mocks are created with the @Mock annotation and the tested object with the @TestSubject one (which will get its dependencies injected from created mocks). The tested object must be created in-line.

@RunWith(EasyMockRunner.class) public class LoginControllerTest { @Mock private LoginDao loginDao; @Mock private LoginService loginService; @TestSubject private LoginController loginController = new LoginController(); }

3.3. JMockit

For JMockit we'll be using version 1.24 (Javadoc) as version 1.25 hasn't been released yet (at least while writing this).

Setup for JMockit is as easy as with Mockito, with the exception that there is no specific annotation for partial mocks (and really no need either) and that you must use JMockit as the test runner.

Mocks are defined using the @Injectable annotation (that will create only one mock instance) or with @Mocked annotation (that will create mocks for every instance of the class of the annotated field).

The tested instance gets created (and its mocked dependencies injected) using the @Tested annotation.

@RunWith(JMockit.class) public class LoginControllerTest { @Injectable private LoginDao loginDao; @Injectable private LoginService loginService; @Tested private LoginController loginController; }

4. Verifying No Calls to Mock

4.1. Mockito

For verifying that a mock received no calls in Mockito, you have the method verifyZeroInteractions() that accepts a mock.

@Test public void assertThatNoMethodHasBeenCalled() { loginController.login(null); Mockito.verifyZeroInteractions(loginService); }

4.2. EasyMock

For verifying that a mock received no calls you simply don't specify behavior, you replay the mock, and lastly, you verify it.

@Test public void assertThatNoMethodHasBeenCalled() { EasyMock.replay(loginService); loginController.login(null); EasyMock.verify(loginService); }

4.3. JMockit

For verifying that a mock received no calls you simply don't specify expectations for that mock and do a FullVerifications(mock) for said mock.

@Test public void assertThatNoMethodHasBeenCalled() { loginController.login(null); new FullVerifications(loginService) {}; }

5. Defining Mocked Method Calls and Verifying Calls to Mocks

5.1. Mockito

For mocking method calls, you can use Mockito.when(mock.method(args)).thenReturn(value). Here you can return different values for more than one call just adding them as more parameters: thenReturn(value1, value2, value-n, …).

Note that you can't mock void returning methods with this syntax. In said cases, you'll use a verification of said method (as shown on line 11).

For verifying calls to a mock you can use Mockito.verify(mock).method(args) and you can also verify that no more calls were done to a mock using verifyNoMoreInteractions(mock).

For verifying args, you can pass specific values or use predefined matchers like any(), anyString(), anyInt(). There are a lot more of that kind of matchers and even the possibility to define your matchers which we'll see in following examples.

@Test public void assertTwoMethodsHaveBeenCalled() { UserForm userForm = new UserForm(); userForm.username = "foo"; Mockito.when(loginService.login(userForm)).thenReturn(true); String login = loginController.login(userForm); Assert.assertEquals("OK", login); Mockito.verify(loginService).login(userForm); Mockito.verify(loginService).setCurrentUser("foo"); } @Test public void assertOnlyOneMethodHasBeenCalled() { UserForm userForm = new UserForm(); userForm.username = "foo"; Mockito.when(loginService.login(userForm)).thenReturn(false); String login = loginController.login(userForm); Assert.assertEquals("KO", login); Mockito.verify(loginService).login(userForm); Mockito.verifyNoMoreInteractions(loginService); }

5.2. EasyMock

For mocking method calls, you use EasyMock.expect(mock.method(args)).andReturn(value).

For verifying calls to a mock, you can use EasyMock.verify(mock), but you must call it always after calling EasyMock.replay(mock).

For verifying args, you can pass specific values, or you have predefined matchers like isA(Class.class), anyString(), anyInt(), and a lot more of that kind of matchers and again the possibility to define your matchers.

@Test public void assertTwoMethodsHaveBeenCalled() { UserForm userForm = new UserForm(); userForm.username = "foo"; EasyMock.expect(loginService.login(userForm)).andReturn(true); loginService.setCurrentUser("foo"); EasyMock.replay(loginService); String login = loginController.login(userForm); Assert.assertEquals("OK", login); EasyMock.verify(loginService); } @Test public void assertOnlyOneMethodHasBeenCalled() { UserForm userForm = new UserForm(); userForm.username = "foo"; EasyMock.expect(loginService.login(userForm)).andReturn(false); EasyMock.replay(loginService); String login = loginController.login(userForm); Assert.assertEquals("KO", login); EasyMock.verify(loginService); }

5.3. JMockit

With JMockit, you have defined steps for testing: record, replay and verify.

Record is done in a new Expectations(){{}} block (into which you can define actions for several mocks), replay is done simply by invoking a method of the tested class (that should call some mocked object), and verification is done inside a new Verifications(){{}} block (into which you can define verifications for several mocks).

For mocking method calls, you can use mock.method(args); result = value; inside any Expectations block. Here you can return different values for more than one call just using returns(value1, value2, …, valuen); instead of result = value;.

For verifying calls to a mock you can use new Verifications(){{mock.call(value)}} or new Verifications(mock){{}} to verify every expected call previously defined.

For verifying args, you can pass specific values, or you have predefined values like any, anyString, anyLong, and a lot more of that kind of special values and again the possibility to define your matchers (that must be Hamcrest matchers).

@Test public void assertTwoMethodsHaveBeenCalled() { UserForm userForm = new UserForm(); userForm.username = "foo"; new Expectations() {{ loginService.login(userForm); result = true; loginService.setCurrentUser("foo"); }}; String login = loginController.login(userForm); Assert.assertEquals("OK", login); new FullVerifications(loginService) {}; } @Test public void assertOnlyOneMethodHasBeenCalled() { UserForm userForm = new UserForm(); userForm.username = "foo"; new Expectations() {{ loginService.login(userForm); result = false; // no expectation for setCurrentUser }}; String login = loginController.login(userForm); Assert.assertEquals("KO", login); new FullVerifications(loginService) {}; }

6. Mocking Exception Throwing

6.1. Mockito

Exception throwing can be mocked using .thenThrow(ExceptionClass.class) after a Mockito.when(mock.method(args)).

@Test public void mockExceptionThrowin() { UserForm userForm = new UserForm(); Mockito.when(loginService.login(userForm)).thenThrow(IllegalArgumentException.class); String login = loginController.login(userForm); Assert.assertEquals("ERROR", login); Mockito.verify(loginService).login(userForm); Mockito.verifyZeroInteractions(loginService); }

6.2. EasyMock

Exception throwing can be mocked using .andThrow(new ExceptionClass()) after an EasyMock.expect(…) call.

@Test public void mockExceptionThrowing() { UserForm userForm = new UserForm(); EasyMock.expect(loginService.login(userForm)).andThrow(new IllegalArgumentException()); EasyMock.replay(loginService); String login = loginController.login(userForm); Assert.assertEquals("ERROR", login); EasyMock.verify(loginService); }

6.3. JMockit

Mocking exception throwing with JMockito is especially easy. Just return an Exception as the result of a mocked method call instead of the “normal” return.

@Test public void mockExceptionThrowing() { UserForm userForm = new UserForm(); new Expectations() {{ loginService.login(userForm); result = new IllegalArgumentException(); // no expectation for setCurrentUser }}; String login = loginController.login(userForm); Assert.assertEquals("ERROR", login); new FullVerifications(loginService) {}; }

7. Mocking an Object to Pass Around

7.1. Mockito

You can create a mock also to pass as an argument for a method call. With Mockito, you can do that with a one-liner.

@Test public void mockAnObjectToPassAround() { UserForm userForm = Mockito.when(Mockito.mock(UserForm.class).getUsername()) .thenReturn("foo").getMock(); Mockito.when(loginService.login(userForm)).thenReturn(true); String login = loginController.login(userForm); Assert.assertEquals("OK", login); Mockito.verify(loginService).login(userForm); Mockito.verify(loginService).setCurrentUser("foo"); }

7.2. EasyMock

Mocks can be created in-line with EasyMock.mock(Class.class). Afterward, you can use EasyMock.expect(mock.method()) to prepare it for execution, always remembering to call EasyMock.replay(mock) before using it.

@Test public void mockAnObjectToPassAround() { UserForm userForm = EasyMock.mock(UserForm.class); EasyMock.expect(userForm.getUsername()).andReturn("foo"); EasyMock.expect(loginService.login(userForm)).andReturn(true); loginService.setCurrentUser("foo"); EasyMock.replay(userForm); EasyMock.replay(loginService); String login = loginController.login(userForm); Assert.assertEquals("OK", login); EasyMock.verify(userForm); EasyMock.verify(loginService); }

7.3. JMockit

To mock an object for just one method, you can simply pass it mocked as a parameter to the test method. Then you can create expectations as with any other mock.

@Test public void mockAnObjectToPassAround(@Mocked UserForm userForm) { new Expectations() {{ userForm.getUsername(); result = "foo"; loginService.login(userForm); result = true; loginService.setCurrentUser("foo"); }}; String login = loginController.login(userForm); Assert.assertEquals("OK", login); new FullVerifications(loginService) {}; new FullVerifications(userForm) {}; }

8. Custom Argument Matching

8.1. Mockito

Sometimes argument matching for mocked calls needs to be a little more complex than just a fixed value or anyString(). For that cases with Mockito has its matcher class that is used with argThat(ArgumentMatcher).

@Test public void argumentMatching() { UserForm userForm = new UserForm(); userForm.username = "foo"; // default matcher Mockito.when(loginService.login(Mockito.any(UserForm.class))).thenReturn(true); String login = loginController.login(userForm); Assert.assertEquals("OK", login); Mockito.verify(loginService).login(userForm); // complex matcher Mockito.verify(loginService).setCurrentUser(ArgumentMatchers.argThat( new ArgumentMatcher() { @Override public boolean matches(String argument) { return argument.startsWith("foo"); } } )); }

8.2. EasyMock

Custom argument matching is a little bit more complicated with EasyMock as you need to create a static method in which you create the actual matcher and then report it with EasyMock.reportMatcher(IArgumentMatcher).

Once this method is created, you use it on your mock expectation with a call to the method (like seen in the example in line ).

@Test public void argumentMatching() { UserForm userForm = new UserForm(); userForm.username = "foo"; // default matcher EasyMock.expect(loginService.login(EasyMock.isA(UserForm.class))).andReturn(true); // complex matcher loginService.setCurrentUser(specificArgumentMatching("foo")); EasyMock.replay(loginService); String login = loginController.login(userForm); Assert.assertEquals("OK", login); EasyMock.verify(loginService); } private static String specificArgumentMatching(String expected) { EasyMock.reportMatcher(new IArgumentMatcher() { @Override public boolean matches(Object argument) { return argument instanceof String && ((String) argument).startsWith(expected); } @Override public void appendTo(StringBuffer buffer) { //NOOP } }); return null; }

8.3. JMockit

Custom argument matching with JMockit is done with the special withArgThat(Matcher) method (that receives Hamcrest‘s Matcher objects).

@Test public void argumentMatching() { UserForm userForm = new UserForm(); userForm.username = "foo"; // default matcher new Expectations() {{ loginService.login((UserForm) any); result = true; // complex matcher loginService.setCurrentUser(withArgThat(new BaseMatcher() { @Override public boolean matches(Object item) { return item instanceof String && ((String) item).startsWith("foo"); } @Override public void describeTo(Description description) { //NOOP } })); }}; String login = loginController.login(userForm); Assert.assertEquals("OK", login); new FullVerifications(loginService) {}; }

9. Partial Mocking

9.1. Mockito

Mockito allows partial mocking (a mock that uses the real implementation instead of mocked method calls in some of its methods) in two ways.

You can either use .thenCallRealMethod() in a normal mock method call definition, or you can create a spy instead of a mock in which case the default behavior for that will be to call the real implementation in all non-mocked methods.

@Test public void partialMocking() { // use partial mock loginController.loginService = spiedLoginService; UserForm userForm = new UserForm(); userForm.username = "foo"; // let service's login use implementation so let's mock DAO call Mockito.when(loginDao.login(userForm)).thenReturn(1); String login = loginController.login(userForm); Assert.assertEquals("OK", login); // verify mocked call Mockito.verify(spiedLoginService).setCurrentUser("foo"); }

9.2. EasyMock

Partial mocking also gets a little more complicated with EasyMock, as you need to define which methods will be mocked when creating the mock.

This is done with EasyMock.partialMockBuilder(Class.class).addMockedMethod(“methodName”).createMock(). Once this is done, you can use the mock as any other non-partial mock.

@Test public void partialMocking() { UserForm userForm = new UserForm(); userForm.username = "foo"; // use partial mock LoginService loginServicePartial = EasyMock.partialMockBuilder(LoginService.class) .addMockedMethod("setCurrentUser").createMock(); loginServicePartial.setCurrentUser("foo"); // let service's login use implementation so let's mock DAO call EasyMock.expect(loginDao.login(userForm)).andReturn(1); loginServicePartial.setLoginDao(loginDao); loginController.loginService = loginServicePartial; EasyMock.replay(loginDao); EasyMock.replay(loginServicePartial); String login = loginController.login(userForm); Assert.assertEquals("OK", login); // verify mocked call EasyMock.verify(loginServicePartial); EasyMock.verify(loginDao); }

9.3. JMockit

Partial mocking with JMockit is especially easy. Every method call for which no mocked behavior has been defined in an Expectations(){{}} uses the “real” implementation.

Now let's imagine that we want to partially mock the LoginService class to mock the setCurrentUser() method while using the actual implementation of the login() method.

To do this, we first create and pass an instance of LoginService to the expectations block. Then, we only record an expectation for the setCurrentUser() method:

@Test public void partialMocking() { LoginService partialLoginService = new LoginService(); partialLoginService.setLoginDao(loginDao); loginController.loginService = partialLoginService; UserForm userForm = new UserForm(); userForm.username = "foo"; new Expectations(partialLoginService) {{ // let's mock DAO call loginDao.login(userForm); result = 1; // no expectation for login method so that real implementation is used // mock setCurrentUser call partialLoginService.setCurrentUser("foo"); }}; String login = loginController.login(userForm); Assert.assertEquals("OK", login); // verify mocked call new Verifications() {{ partialLoginService.setCurrentUser("foo"); }}; }

10. Conclusion

In this post, we've been comparing three Java mock libraries, each one with its strong points and downsides.

  • All three of them are easily configured with annotations to help you define mocks and the object-under-test, with runners to make mock injection as painless as possible.
    • We'd say Mockito would win here as it has a special annotation for partial mocks, but JMockit doesn't even need it, so let's say that it's a tie between those two.
  • All three of them follow more or less the record-replay-verify pattern, but in our opinion, the best one to do so is JMockit as it forces you to use those in blocks, so tests get more structured.
  • Easiness of use is important so you can work as less as possible to define your tests. JMockit will be the chosen option for its fixed-always-the-same structure.
  • Mockito is more or less THE most known so that the community will be bigger.
  • Having to call replay every time you want to use a mock is a clear no-go, so we'll put a minus one for EasyMock.
  • Konsistensi / kesederhanaan juga penting bagi saya. Kami menyukai cara mengembalikan hasil JMockit yang sama untuk hasil "normal" seperti untuk pengecualian.

Akankah semua ini dikatakan, kami akan memilih JMockit sebagai semacam pemenang meskipun sampai sekarang kami telah menggunakan Mockito karena kami telah terpikat oleh kesederhanaan dan strukturnya yang tetap dan akan mencoba dan menggunakannya mulai sekarang di.

The implementasi penuh dari tutorial ini dapat ditemukan pada proyek GitHub sehingga merasa bebas untuk men-download dan bermain dengan itu.