Mockito vs EasyMock vs JMockit

1. Pengenalan

1.1. Gambaran keseluruhan

Dalam catatan ini, kita akan membincangkan tentang mengejek : apa itu, mengapa menggunakannya dan beberapa contoh bagaimana mengejek kes ujian yang sama menggunakan beberapa perpustakaan tiruan yang paling banyak digunakan untuk Java.

Kita akan mulakan dengan beberapa definisi konsep mengejek formal / separa formal; maka kami akan membentangkan kes yang sedang diuji, menindaklanjuti dengan contoh untuk setiap perpustakaan dan berakhir dengan beberapa kesimpulan. Perpustakaan yang dipilih adalah Mockito, EasyMock, dan JMockit.

Sekiranya anda merasa sudah mengetahui asas-asas mengejek, mungkin anda boleh melangkau ke Titik 2 tanpa membaca tiga perkara seterusnya.

1.2. Sebab-Sebab Menggunakan Mock

Kami akan mula mengandaikan bahawa anda sudah membuat kod mengikuti beberapa metodologi pembangunan yang didorong berpusat pada ujian (TDD, ATDD atau BDD). Atau sekadar anda mahu membuat ujian untuk kelas yang ada yang bergantung pada pergantungan untuk mencapai fungsinya.

Walau bagaimanapun, semasa menguji unit kelas, kami ingin menguji hanya fungsinya dan bukan kebergantungannya (sama ada kerana kami mempercayai pelaksanaannya atau kerana kami akan mengujinya sendiri).

Untuk mencapai ini, kita perlu memberikan kepada objek yang sedang diuji, penggantian yang dapat kita kendalikan untuk ketergantungan itu. Dengan cara ini kita dapat memaksa nilai pulangan yang melampau, pengecualian melemparkan atau hanya mengurangkan kaedah yang memakan masa menjadi nilai pulangan tetap.

Penggantian terkawal ini adalah tiruan , dan ia akan membantu anda mempermudah pengekodan ujian dan mengurangkan masa pelaksanaan ujian.

1.3. Konsep dan Definisi Mengejek

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

  • Objek palsu disalurkan tetapi tidak pernah digunakan. Biasanya, mereka hanya digunakan untuk mengisi senarai parameter.
  • Objek palsu mempunyai implementasi yang berfungsi, tetapi biasanya, mengambil jalan pintas yang menjadikannya tidak sesuai untuk dihasilkan (dalam pangkalan data memori adalah contoh yang baik).
  • Stub memberikan jawapan dalam panggilan untuk panggilan yang dibuat semasa ujian, biasanya tidak memberi respons sama sekali terhadap apa-apa di luar apa yang diprogramkan untuk ujian. Stub juga dapat merakam maklumat mengenai panggilan, seperti rintisan gerbang e-mel yang mengingati mesej yang 'dihantar', atau mungkin hanya berapa banyak pesan yang 'dikirim'.
  • Mock adalah apa yang kita bicarakan di sini: objek yang diprogramkan dengan jangkaan yang membentuk spesifikasi panggilan yang diharapkan mereka terima.

1.4 Untuk Mengejek atau Tidak Mengejek: Itulah Soalannya

Tidak semuanya mesti diejek . Kadang-kadang lebih baik untuk melakukan ujian integrasi kerana mengejek bahawa kaedah / ciri hanya berfungsi untuk keuntungan sebenarnya. Dalam kes ujian kami (yang akan ditunjukkan pada titik seterusnya) yang akan menguji LoginDao .

The LoginDao akan menggunakan beberapa perpustakaan pihak ketiga untuk akses DB, dan mengejek ia hanya akan terdiri atas jaminan bahawa parameter telah disiapkan bagi panggilan, tetapi kami masih perlu ujian bahawa panggilan mengembalikan data yang kita mahu.

Atas sebab itu, ini tidak akan dimasukkan dalam contoh ini (walaupun kami dapat menulis kedua-dua ujian unit dengan panggilan palsu untuk panggilan perpustakaan pihak ketiga DAN ujian integrasi dengan DBUnit untuk menguji prestasi sebenar perpustakaan pihak ketiga).

2. Kes Ujian

Dengan mempertimbangkan semua perkara di bahagian sebelumnya, mari kita mencadangkan kes ujian yang agak biasa dan bagaimana kita akan mengujinya menggunakan ejekan (apabila masuk akal untuk menggunakan ejekan). Ini akan membantu kita untuk mempunyai senario yang sama untuk kemudiannya dapat membandingkan perpustakaan mengejek yang berbeza.

2.1 Cadangan Kes

Kes ujian yang dicadangkan akan menjadi proses masuk dalam aplikasi dengan seni bina berlapis.

Permintaan log masuk akan dikendalikan oleh pengawal, yang menggunakan perkhidmatan, yang menggunakan DAO (yang mencari kelayakan pengguna di DB). Kami tidak akan memperdalam pelaksanaan setiap lapisan dan akan lebih memfokuskan pada interaksi antara komponen setiap lapisan.

Dengan cara ini, kita akan mempunyai LoginController , LoginService dan LoginDAO . Mari lihat gambarajah untuk penjelasan:

2.2 Pelaksanaan

Kami akan mengikuti sekarang dengan pelaksanaan yang digunakan untuk kes ujian, sehingga kami dapat memahami apa yang berlaku (atau apa yang harus terjadi) pada ujian.

Kami akan mulakan dengan model yang digunakan untuk semua operasi, UserForm , yang hanya akan menyimpan nama pengguna dan kata laluan (kami menggunakan pengubah akses awam untuk mempermudah) dan kaedah mendapatkan untuk bidang nama pengguna untuk membolehkan mengejek harta itu:

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

Mari ikuti dengan LoginDAO , yang tidak akan berfungsi kerana kami hanya mahu kaedahnya ada di sana sehingga kami dapat mengejeknya apabila diperlukan:

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

LoginDao akan digunakan oleh LoginService dalam kaedah log masuknya . LoginService juga akan mempunyai kaedah setCurrentUser yang mengembalikan kekosongan untuk menguji ejekan itu.

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; } } }

Akhirnya, LoginController akan menggunakan LoginService untuk kaedah log masuknya . Ini termasuk:

  • kes di mana tidak ada panggilan ke perkhidmatan ejekan yang akan dilakukan.
  • kes di mana hanya satu kaedah akan dipanggil.
  • kes di mana semua kaedah akan dipanggil.
  • kes di mana pembuangan pengecualian 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 kita telah melihat apa yang cuba kita uji, mari kita lihat bagaimana kita mengolok-olokkannya dengan setiap perpustakaan.

3. Persediaan Ujian

3.1 Mockito

Untuk Mockito kami akan menggunakan versi 2.8.9.

Cara yang paling mudah untuk mencipta dan menggunakan ejek-ejekan adalah melalui @Mock dan @InjectMocks penjelasan. Yang pertama akan membuat tiruan untuk kelas yang digunakan untuk menentukan medan dan yang kedua akan mencuba memasukkan ejekan yang dibuat ke dalam ejekan yang diberi penjelasan.

Terdapat lebih banyak anotasi seperti @Spy yang membolehkan anda membuat tiruan separa (ejekan yang menggunakan pelaksanaan biasa dalam kaedah tidak diejek).

Walaupun begitu, anda perlu memanggil MockitoAnnotations.initMocks (ini) sebelum menjalankan sebarang ujian yang akan menggunakan ejekan tersebut agar semua "sihir" ini berfungsi. Ini biasanya dilakukan dengan kaedah @Sebelum dijelaskan. Anda juga boleh 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 bahawa dengan EasyMock, agar ejekan mulai "berfungsi", anda mesti memanggil EasyMock.replay (mock) pada setiap kaedah ujian, 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.
  • Ketekalan / kesederhanaan juga penting bagi saya. Kami menyukai cara mengembalikan hasil JMockit yang sama untuk hasil "normal" seperti pengecualian.

Adakah dengan semua ini, kita akan memilih JMockit sebagai pemenang walaupun hingga sekarang kita telah menggunakan Mockito kerana kita terpikat dengan kesederhanaan dan strukturnya yang tetap dan akan mencuba dan menggunakannya dari sekarang pada.

The pelaksanaan penuh tutorial ini boleh ditemui di atas projek GitHub yang jadi berasa bebas untuk memuat turun dan bermain dengannya.