Skip to content

Porównanie Framework'ów Testowych Mockito oraz EasyMock

Damian Muszyński edited this page Apr 19, 2021 · 7 revisions

Porównanie Framework'ów Testowych Mockito oraz EasyMock

Przeglądając różne artykuły internetowe zawierające informacje o tym, który framework do mockowania jest najlepszy najczęściej trafiałem na opis i porównania frameworków Mockito i EasyMock. Dlatego też postanowiłem wybrać owe biblioteki do wspomagania testów wykonywanych za pomocą JUnit oraz TestNG i porównać je same pod względem użyteczności, różnic czy też poziomu skomplikowania.

Schemat testu wykonanywanego za pomocą frameworku Mockito oraz EasyMock

Tutaj zaprezentowane zostaną przykładowe schematy testowe, ukazujące jak wykonuje się testy dla frameworku Mockito oraz EasyMock i ukazujące ich różnice.

Prosty test wykonany za pomocą Mockito

import static org.mockito.Mockito.*;

List mock = mock(List.class);

when(mock.get(0)).thenReturn("one");
when(mock.get(1)).thenReturn("two");

someCodeThatInteractsWithMock();

verify(mock).clear();                       

Prosty test wykonany za pomocą EasyMock

import static org.easymock.classextension.EasyMock.*;

List mock = createNiceMock(List.class);

expect(mock.get(0)).andStubReturn("one");
expect(mock.get(1)).andStubReturn("two");
mock.clear();

replay(mock);

someCodeThatInteractsWithMock();

verify(mock); 

Różnice implementacyjne przy różnych przypadkach użycia

Tu przedstawione zostaną różnice w implementacji różnych przypadków, w których wykorzystuje się frameworki testowe Mockito oraz EasyMock.

Stubbing metod typu void - Mockito

List mock = mock(List.class);

doThrow(new RuntimeException()).when(mock).clear();

Stubbing metod typu void - EasyMock

List mock = createNiceMock(List.class);

mock.clear();
expectLastCall().andThrow(new RuntimeException());

replay(mock);

Rzucanie wyjątków - Mockito

Zgłaszanie wyjątków można wyszydzać za pomocą .thenThrow (ExceptionClass.class) po 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);
}

Rzucanie wyjątków - EasyMock

Rzucanie wyjątków można wyśmiewać za pomocą .andThrow (new ExceptionClass ()) po wywołaniu EasyMock.expect (…).

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

Dopasowywanie argumentów niestandardowych - Mockito

Czasami dopasowywanie argumentów dla wywołań pozorowanych musi być trochę bardziej złożone niż tylko ustalona wartość lub anyString (). W takich przypadkach Mockito ma swoją klasę dopasowującą, która jest używana z 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<String>() {
            @Override
            public boolean matches(String argument) {
                return argument.startsWith("foo");
            }
        }
    ));
}

Dopasowywanie argumentów niestandardowych - EasyMock

Dopasowanie niestandardowych argumentów jest nieco bardziej skomplikowane w EasyMock, ponieważ musisz utworzyć statyczną metodę, w której tworzysz rzeczywisty element dopasowujący, a następnie raportujesz go za pomocą EasyMock.reportMatcher (IArgumentMatcher).

Po utworzeniu tej metody używasz jej na pozorowanym oczekiwaniu z wywołaniem metody (jak widać w przykładzie w wierszu).

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

Nie możesz „szpiegować” za pomocą EasyMock

Mockito ma fajną funkcję zwaną szpiegowaniem. Tworzysz rzeczywisty obiekt (nie makietę) i szpiegujesz go. Zasadniczo oznacza to, że obiekt będzie się zachowywał normalnie, ale wszystkie wywołania metod będą rejestrowane, co pozwoli na ich późniejszą weryfikację. Oto przykład:

Przykład użycia dla Mockito

List list = new LinkedList(); // real object
   List spy = spy(list); // spy wrapper

   //optionally, you can stub out some methods:
   when(spy.size()).thenReturn(100);

   //this method call is spied
   spy.add("one");

   // since a real add was done, get(0) will return "one"
   assertEquals("one", spy.get(0));

   //size() method was stubbed and to return 100
   assertEquals(100, spy.size());

   //optionally, you can verify that add was called as expected
   verify(spy).add("one");

W EasyMock nie ma szpiegowania jako takiego. Próbowałem jednak pomyśleć o przypadkach użycia i doszedłem do wniosku, że używając podzbioru funkcji przechwytywania EasyMock, częściowego mockowania i delegowania, powinienem być w stanie objąć je wszystkie. Może to być jednak bardziej uciążliwe. Ale myślę, że takie przypadki użycia są dość rzadkie. Nie jestem przeciwny dodawaniu funkcji szpiegowskiej, jeśli nie okaże się to błędne.

Jeśli chodzi o powyższy przykład, musimy pomyśleć o celu tego testu. Powiedzmy, że w tym przypadku mamy jakiś stary kod, dla którego chcemy się upewnić, że metoda add jest wywoływana z odpowiednim parametrem, ale chcemy zachować jej zwykłe zachowanie. Jest to dość rzadkie, ale może się zdarzyć. Możemy to zrobić w ten sposób:

Przykład użycia dla EasyMock

// Real object
    List real = new LinkedList();
    // create the mock
    List list = createMock(List.class);

    // spy the parameter and perform the normal behavior
    Capture c = new Capture();
    expect(list.add(capture(c))).andDelegateTo(real);

    replayAll();

    // the actual test
    list.add("one");

    // get will return what was expected
    assertEquals("one", real.get(0));

    // check the capture (will throw an error if nothing was captured)
    assertEquals("one", c.getValue());

    // this is unnecessary since checking the capture is enough
    verifyAll();

EasyMock częściej psuje testy

Tak. Tak to powinno byc. Przynajmniej taki jest mój punkt widzenia. To naprawdę jedna z głównych różnic. Z EasyMock będziesz musiał oczekiwać wszystkiego. Więc twoje testy się zepsują, gdy tylko coś zmienisz. Z Mockito zweryfikujesz, czego chcesz i tylko te weryfikacje mogą się zepsuć.

Co za różnica? EasyMock zmusza Cię do oczekiwania wszystkiego. Twoje testy się wtedy zepsują i będziesz zmuszony wrócić do każdego z nich i zadać sobie pytanie „Hum, czy to miało się zepsuć?”. Mockito nie zmusi Cię do niczego. Tak więc test zweryfikuje tylko to, co pierwotna myśl programisty byłaby istotna. Ale wtedy, pracując nad inną częścią kodu, nie będziesz wiedział, że faktycznie brakuje weryfikacji. Nie będziesz zmuszony spojrzeć na stare testy. Wszystkie testy pozostaną zielone, a paskudne błędy wkradną się do środka. Dla mnie to główna różnica. Kompromis między szybszym napisaniem testu a testem sprawdzającym nieoczekiwane. Spójrzmy na przykład, który to ilustruje.

Przykład użycia dla Mockito

//mock creation
 List mockedList = mock(List.class);

 // call the mock in some business code
 mockedList.add("one"); // the return value isn't used nor specified
 // mockedList.clear(); // adding this call in the business code WON'T break the test

 //verification that add() was called with the right parameter
 verify(mockedList).add("one");

Przykład użycia dla EasyMock

//mock creation
 List mockedList = createMock(List.class);
 // We are forced to return a meaningful value
 expect(mockedList.add("one").andReturn(true);
 replayAll();

 // call the mock in some business code
 mockedList.add("one"); // the return value isn't used by was specified
 // mockedList.clear(); // adding this call in the business code will break the test

 //verification that add() was called with the right parameter
 // and that no other methods were called
 verifyAll();

Przykład EasyMock testuje to, co było zamierzone, ale robi również dwie rzeczy za kulisami.

Upewnienie się, że makieta dokładnie mockuje listę, wymuszając zwrócenie wartości Zapobieganie działaniu testu, jeśli inne wywołania makiet są wykonywane przez kod biznesowy. To zmusi nas do zastanowienia się, czy złamanie tego testu było oczekiwane przez naszą refaktoryzację.

Clone this wiki locally