Spock vs Junit i Mockito
Zaktualizowaliśmy ten tekst dla Ciebie!
Data aktualizacji: 19.12.2024
Autor aktualizacji: Mateusz Morawski
Wstęp
Spock i JUnit to frameworki do testowania jednostkowego aplikacji Javowych. Mockito to znana i stabilna biblioteka dodająca możliwość mockowania dla testów JUnit, pozwalająca pisać testy w języku Java. Z kolei Spock to kompletny framework testowy oparty na JUnit, zaprojektowany do testowania aplikacji Java, ale testy pisane są w Groovym. Mockito ma znacznie dłuższą historię, podczas gdy stabilna wersja Spocka została wydana w marcu 2015 roku. Spock jest więc stosunkowo młody, ale ma świetlaną przyszłość. W tym artykule spróbuję porównać JUnit ze Spockiem, nie mogę jednak obiecać, że będę neutralnym sędzią, ponieważ jestem zwolennikiem Spocka.
Wszystkie przykłady omówione w tym artykule są dostępne tutaj:
github: https://github.com/wmaziarz/jlabs-spock-mockito
[Uwaga]: repozytorium kodu jest już przestarzałe – pojawiły się nowe wersje Spocka, JUnit ma już wersję 5
Proste przykłady testów
Oba testy jednostkowe powinny testować klasę DefaultMassMailSender oraz jej publiczną metodę, która powinna wybierać użytkowników o nazwach zaczynających się od określonego ciągu znaków i wysyłać do nich wiadomości e-mail z podanym tematem i treścią.
JUnit i Mockito:
/**
* should send emails to matching users and return a list of email addresses that the email has been sent to
*/
@Test
public void testSendingEmailsToMatchingUsers() {
//given:
User user1 = new User();
user1.setId(1L);
user1.setUserName("test user one");
user1.setEmail("test@user.one");
User user2 = new User();
user2.setId(2L);
user2.setUserName("IGNORED test user two");
user2.setEmail("test@user.two");
User user3 = new User();
user3.setId(3L);
user3.setUserName("THIRD test user");
user3.setEmail("test@user.three");
User user4 = new User();
user4.setId(4L);
user4.setUserName("testing user four");
user4.setEmail("test@user.four");
String subject = "some test email subject";
String content = "some content of the email";
when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2, user3, user4));
//when:
List<String> result = underTest.sendEmailsToUsers(subject, content, "test");
//then:
Assert.assertEquals(Arrays.asList("test@user.one", "test@user.four"), result);
verify(userRepository).findAll();
}
Spock:
def "should send emails to matching users and return a list of email addresses that the email has been sent to"() {
given:
def user1 = new User(id: 1L, userName: 'test user one', email: 'test@user.one')
def user2 = new User(id: 2L, userName: 'IGNORED test user two', email: 'test@user.two')
def user3 = new User(id: 3L, userName: 'THIRD test user', email: 'test@user.three')
def user4 = new User(id: 4L, userName: 'testing user four', email: 'test@user.four')
def subject = 'some test email subject'
def content = 'some content of the email'
when:
def result = underTest.sendEmailsToUsers(subject, content, 'test')
then: "get users from repository and send email to each matching user"
1 * userRepository.findAll() >> [user1, user2, user3, user4]
1 * emailSender.sendEmail(subject, content, 'test@user.one')
1 * emailSender.sendEmail(subject, content, 'test@user.four')
result == ['test@user.one', 'test@user.four']
}
Oba testy robią to samo. Jaka jest więc główna różnica w kodzie między tymi testami jednostkowymi?
- Testy Spock są krótsze: to niezaprzeczalny fakt. Spock – 21 linii kodu w porównaniu do Mockito – 36 linii kodu. W JUnit konieczne jest wywoływanie konstruktorów, setterów oraz budowanie kolekcji w sposób typowy dla Javy. Kod testu w JUnit musi być bardzo rozbudowany, ponieważ jest pisany w czystym języku Java. Natomiast w Spocku nie musisz tracić czasu na pisanie wszystkich tych elementów, które zaśmiecają kod i czynią go mniej czytelnym. Pisząc test Spock, możesz skupić się na samym teście, a wszystkie te pomocnicze części kodu są ograniczone do minimum.
- Spock to framework, który obejmuje: testowanie jednostkowe, testowanie integracyjne, mockowanie i stubbing. Oprócz tego jest to specyfikacja aplikacji. JUnit to jedynie framework do testowania jednostkowego i wymaga dodania Mockito (lub innej biblioteki do mockowania), jeśli potrzebna jest możliwość tworzenia atrap.
- Kod w Spocku jest bardziej elegancki. Ponadto jest zwarty i znacznie mniej rozwlekły niż kod w Mockito. Jest to kwestia gustu, aczkolwiek wierzę, że wielu programistów zgodziłoby się z tą opinią.
- Testy JUnit są pisane w czystej Javie, która jest dobrze znana programistom programującym w tym języku. Testy Spock są pisane w Groovy, co może niektórym programistom Javy się nie podobać. Nie ma się jednak czego obawiać – Groovy nie jest taki zły. Co więcej, oficjalna dokumentacja Spocka jest obszerna i pomocna. W Internecie jest również coraz więcej zasobów do nauki Spocka i Groovy.
- Kod napisany w Spock jest bardziej czytelny i samoopisujący. Zaczynając od nazwy metody, która w JUnit musi spełniać pewne ścisłe konwencje nazewnictwa, przy czym jeśli chcesz opisać test, musisz dodać komentarz. W przeciwieństwie do tego, nazwa metody w Spock może być dowolnym wybranym przez Ciebie ciągiem znaków. Części testu w Spock są wyraźnie oddzielone etykietami, takimi jak 'given’, 'when’, 'then’ (są też inne dostępne), podczas gdy w przypadku testu JUnit nie ma takiego rozdzielenia, chyba że programista doda jakieś komentarze. Etykiety w Spock mogą być używane w raportach testów generowanych przez dedykowane wtyczki, aby uczynić je bardziej wartościowymi. Natomiast komentarze w kodzie JUnit są pomocne tylko dla programistów pracujących nad tym kodem.
- Testy Spock rozszerzają klasę spock.lang.Specification z określonego powodu. Jest to forma dokumentacji i specyfikacji testowanego kodu. Składnia Spocka, nazwy metod, etykiety – wszystkie te elementy pomagają osiągnąć cel, jakim jest: test będący rzeczywiście specyfikacją kodu. Czy kiedykolwiek próbowałeś przeczytać test JUnit, aby zrozumieć, jak powinna działać testowana aplikacja?
- Test Spock może być zrozumiały nie tylko przez programistów, którzy często żyją w swojej własnej galaktyce i są uznawani za dziwnych przez innych ludzi :). Spróbuj pokazać Specyfikację Spocka niektórym analitykom biznesowym i poproś ich o jej przeczytanie. Następnie spróbuj zrobić to samo z testem JUnit…
Testowanie sterowane danymi
Prawdziwą siłę Spocka widać, gdy chcesz napisać bardziej złożony test i przeprowadzać ten sam test wielokrotnie z różnymi danymi wejściowymi i oczekiwanymi wynikami.
Taki test można napisać w JUnit przy użyciu Parametrized runner. W Spocku wystarczy zdefiniować tabelę danych w sekcji 'where’. Spójrz na poniższe przykłady.
JUnit i Mockito:
@RunWith(Parameterized.class)
public class DefaultMassMailSenderParametrizedMockitoTest {
private UserRepository userRepository;
private EmailSender emailSender;
private DefaultMassMailSender underTest;
private String userName;
private List<String> expectedEmails;
public DefaultMassMailSenderParametrizedMockitoTest(String userName, List<String> expectedEmails) {
this.userName = userName;
this.expectedEmails = expectedEmails;
}
@Before
public void setUp() {
userRepository = mock(UserRepository.class);
emailSender = mock(EmailSender.class);
underTest = new DefaultMassMailSender();
underTest.setUserRepository(userRepository);
underTest.setEmailSender(emailSender);
}
@Parameterized.Parameters
public static List<Object[]> expectedUserEmails() {
return Arrays.asList(new Object[][] {
{"test", Arrays.asList("test@user.one", "test@user.four")},
{"testing", Collections.singletonList("test@user.four")},
{"z", Collections.emptyList()},
{"third", Collections.singletonList("test@user.three")}
});
}
/**
* should send emails to matching users and return a list of email addresses that the email has been sent to - parametrized test
*/
@Test
public void testSendingEmailsToMatchingUsers() {
//given:
User user1 = new User();
user1.setId(1L);
user1.setUserName("test user one");
user1.setEmail("test@user.one");
User user2 = new User();
user2.setId(2L);
user2.setUserName("IGNORED test user two");
user2.setEmail("test@user.two");
User user3 = new User();
user3.setId(3L);
user3.setUserName("THIRD test user");
user3.setEmail("test@user.three");
User user4 = new User();
user4.setId(4L);
user4.setUserName("testing user four");
user4.setEmail("test@user.four");
String subject = "some test email subject";
String content = "some content of the email";
when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2, user3, user4));
//when:
List<String> result = underTest.sendEmailsToUsers(subject, content, userName);
//then:
Assert.assertEquals(expectedEmails, result);
verify(userRepository).findAll();
}
Spock:
def "should send emails to matching users and return a list of email addresses that the email has been sent to - test with data table"() {
given:
def user1 = new User(id: 1L, userName: 'test user one', email: 'test@user.one')
def user2 = new User(id: 2L, userName: 'IGNORED test user two', email: 'test@user.two')
def user3 = new User(id: 3L, userName: 'THIRD test user', email: 'test@user.three')
def user4 = new User(id: 4L, userName: 'testing user four', email: 'test@user.four')
def subject = 'some test email subject'
def content = 'some content of the email'
when:
def result = underTest.sendEmailsToUsers(subject, content, userNameLike)
then:
1 * userRepository.findAll() >> [user1, user2, user3, user4]
_ * emailSender.sendEmail(subject, content, _ as String)
result == expectedEmailList
where:
userNameLike | expectedEmailList
'test' | ['test@user.one', 'test@user.four']
'testing' | ['test@user.four']
'z' | []
'third' | ['test@user.three']
}
Różnica w rozmiarze, złożoności, rozwlekłości i elegancji kodu źródłowego jest zauważalna, prawda? W JUnit konieczne jest utworzenie osobnej klasy JUnit z adnotacją @RunWith(Parameterized.class) oraz stworzenie dwuwymiarowej tablicy w dedykowanej metodzie z adnotacją @Parameterized.Parameters. W Spocku natomiast mamy jedynie sekcję 'where’ z łatwą do odczytania tabelą. W JUnit trudno jest dodać dwa testy parametryzowane w jednej klasie testowej. W Spocku jest to bardzo proste dzięki tabeli danych umieszczonej w opcjonalnej sekcji 'where’ pojedynczej metody testowej, co nie ma wpływu na inne metody testowe znajdujące się w tej samej klasie.
Podsumowanie
Wybór odpowiedniego frameworka testowego dla aplikacji zależy od wielu czynników. W niektórych przypadkach opcje są ograniczone, a czasem nawet nie ma wyboru. Jeśli zaczynasz nowy projekt i masz swobodę w wyborze technologii, rozważ użycie Spocka. Jeśli Twój projekt jest dojrzały, możesz również dodać Spocka obok istniejących testów JUnit. Jeśli Twój projekt jest dojrzały i ma małe pokrycie kodu testami lub nie ma żadnych testów, nic nie powinno Cię powstrzymać przed dodaniem do niego Spock.
Korzystając ze Spocka, szybko napiszesz testy jednostkowe i integracyjne, które będą wyglądać znacznie bardziej elegancko i dostarczą więcej wartości niż stare, dobre testy JUnit.
Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!
Skontaktuj się z nami


