Spock vs Junit i Mockito

Wojciech Maziarz

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