Mockito срещу EasyMock срещу JMockit

1. Въведение

1.1. Общ преглед

В тази публикация ще говорим за подигравки : какво е това, защо да го използваме и няколко примера за подиграване на един и същ тестов случай, използвайки някои от най-използваните подигравателни библиотеки за Java.

Ще започнем с някои формални / полуформални дефиниции на подигравателни понятия; след това ще представим тествания случай, ще проследим примери за всяка библиотека и ще завършим с някои заключения. Избраните библиотеки са Mockito, EasyMock и JMockit.

Ако смятате, че вече знаете основите на подигравките, може би можете да преминете към точка 2, без да четете следващите три точки.

1.2. Причини за използване на подигравки

Ще започнем да приемаме, че вече кодирате следвайки някаква управлявана методология за разработка, центрирана върху тестове (TDD, ATDD или BDD). Или просто, че искате да създадете тест за съществуващ клас, който разчита на зависимости, за да постигне своята функционалност.

Във всеки случай, когато тестваме клас един клас, ние искаме да тестваме само неговата функционалност, а не тази на неговите зависимости (или защото се доверяваме на тяхното изпълнение, или защото ще го тестваме сами).

За да постигнем това, трябва да предоставим на тествания обект заместител, който можем да контролираме за тази зависимост. По този начин можем да принудим екстремни възвръщаеми стойности, хвърляне на изключения или просто да намалим отнемащите време методи до фиксирана възвръщаема стойност.

Тази контролирана подмяна е фиктивна и ще ви помогне да опростите тестовото кодиране и да намалите времето за изпълнение на теста.

1.3. Фиктивни концепции и определение

Нека видим четири определения от статия, написана от Мартин Фаулър, която обобщава основите, които всеки трябва да знае за подигравките:

  • Фиктивни обекти се предават наоколо, но всъщност никога не се използват. Обикновено те се използват само за попълване на списъци с параметри.
  • Фалшивите обекти имат работещи внедрения, но обикновено вземат някакъв пряк път, който ги прави непригодни за производство (добър пример е базата данни в паметта).
  • Stubs осигуряват консервирани отговори на обаждания, направени по време на теста, обикновено изобщо не реагират на нищо извън програмираното за теста. Stubs може също да записва информация за повиквания, като например имейл шлюз, който запомня съобщенията, които е „изпратил“, или може би само колко съобщения е „изпратил“.
  • Тук говорим за подигравки : обекти, предварително програмирани с очаквания, които формират спецификация на обажданията, които се очаква да получат.

1.4 Да се ​​подигравате или да не се подигравате: това е въпросът

Не всичко трябва да се подиграва . Понякога е по-добре да направите тест за интеграция, тъй като подиграването с този метод / функция би било просто работещо с малка действителна полза. В нашия тестов случай (това ще бъде показано в следващата точка), който ще тества LoginDao .

В LoginDao ще използвате някои трети библиотека парти за DB достъп, и да го подиграва ще се състои само от гарантиране, че параметрите са били подготвени за поканата, но ние все още ще трябва да тест, че обаждането връща данните, които искахме.

Поради тази причина той няма да бъде включен в този пример (въпреки че бихме могли да напишем както единичния тест с фалшиви повиквания за извиквания на библиотека на трети страни, така и тест за интеграция с DBUnit за тестване на действителната производителност на библиотеката на трета страна).

2. Тестова кутия

Имайки предвид всичко в предишния раздел, нека предложим доста типичен тестов случай и как ще го тестваме с помощта на макети (когато има смисъл да се използват макети). Това ще ни помогне да имаме общ сценарий, за да можем по-късно да сравним различните подигравателни библиотеки.

2.1 Предложен случай

Предложеният тестов случай ще бъде процесът на влизане в приложение с многослойна архитектура.

Искането за влизане ще се обработва от контролер, който използва услуга, която използва DAO (която търси потребителски идентификационни данни в DB). Няма да задълбочаваме твърде много изпълнението на всеки слой и ще се съсредоточим повече върху взаимодействията между компонентите на всеки слой.

По този начин ще имаме LoginController , LoginService и LoginDAO . Нека видим диаграма за пояснение:

2.2 Прилагане

Сега ще последваме внедряването, използвано за тестовия случай, за да можем да разберем какво се случва (или какво трябва да се случи) на тестовете.

Ще започнем с модела, използван за всички операции, UserForm , който ще съдържа само името и паролата на потребителя (използваме модификатори на публичния достъп за опростяване) и метод за получаване на полето за потребителско име, за да позволи подигравка за това свойство:

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

Нека следваме с LoginDAO , това няма да има функционалност, тъй като искаме само неговите методи да са там, за да можем да се подиграваме с тях, когато е необходимо:

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

LoginDao ще се използва от LoginService в метода му за вход . LoginService също ще има метод setCurrentUser , който връща void, за да тества това подиграване.

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

И накрая, LoginController ще използва LoginService за своя метод за вход . Това ще включва:

  • случай, при който няма да се извършват обаждания до подиграваната услуга.
  • случай, в който ще бъде извикан само един метод.
  • случай, в който ще бъдат извикани всички методи.
  • случай, в който ще се тества хвърляне на изключение.
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"; } } } }

След като видяхме какво се опитваме да тестваме, нека видим как ще се подиграваме с всяка библиотека.

3. Тестова настройка

3.1 Mockito

За Mockito ще използваме версия 2.8.9.

Най-лесният начин за създаване и използване на макети е чрез анотациите @Mock и @InjectMocks . Първият ще създаде макет за класа, използван за дефиниране на полето, а вторият ще се опита да инжектира споменатите създадени макети в анотирания макет.

There are more annotations such as @Spy that lets you create a partial mock (a mock that uses the normal implementation in non-mocked methods).

That being said, you need to call MockitoAnnotations.initMocks(this) before executing any tests that would use said mocks for all of this “magic” to work. This is usually done in a @Before annotated method. You can also use the 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

For EasyMock, we'll be using version 3.4 (Javadoc). Note that with EasyMock, for mocks to start “working”, you must call EasyMock.replay(mock) on every test method, or you will receive an exception.

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.
  • Последователността / простотата също са важни за мен. Хареса ни начинът за връщане на резултатите от JMockit, който е еднакъв за „нормалните“ резултати, както и за изключенията.

Ще се каже ли всичко това, ние ще изберем JMockit като вид победител, въпреки че досега използвахме Mockito, тъй като бяхме пленени от неговата простота и фиксирана структура и ще се опитаме да го използваме от сега На.

Най- пълното прилагане на този урок може да се намери по проекта GitHub да се чувстват свободни да го изтеглите и да играе с него.