Czym jest Spock Framework?
Czym jest Spock Framework?
Spock Framework to alternatywna dla JUnit biblioteka służąca do tworzenia testów jednostkowych. Jest on szczególnie popularny w projektach opartych o języka Java. Zaskoczę Cię jednak, bo sam framework nie korzysta ze składni Java, lecz opiera się o inny język JVM – Groovy. Oznacza to, że kod Twojej aplikacji może pozostać napisany w Javie, a Groovy wykorzystywany jest wyłącznie w testach. Groovy ma znacznie bardziej zwięzłą składnię niż Java – pozwala m.in. pomijać średniki, często nie wymaga jawnego deklarowania typów dzięki def (odpowiednik var w Java), upraszcza tworzenie kolekcji i obiektów oraz oferuje bardziej elastyczną składnię. Spock wykorzystuję tzw. BDD (Behavior Driven Development), czyli tworzenie testów w oparciu o bloki takich sekcji jak given, when, then, expect czy where. Dzęki nim tety często przypominają opis zachowania aplikacji, a nie tylko zestaw wywołań metod i asercji.
- Czym jest Spock Framework?
- Konfiguracja Spock Framework w projekcie Java i Maven
- Pierwszy test jednostkowy w Spock Framework
- Najważniejsze bloki w testach Spock
- Testy parametryzowane w Spock – blok where
- Mockowanie zależności w Spock Framework
- Testowanie wyjątków w Spock Framework
- Spock i Spring Boot
- Wady i zalety Spock
- Podsumowanie
Konfiguracja Spock Framework w projekcie Java i Maven
Aby korzystać ze Spock Framework w projekcie Java opartym na Mavenie, należy przede wszystkim dodać odpowiednie zależności do pliku pom.xml. Kod samej aplikacji nadal może być napisany wyłącznie w Javie – Groovy będzie wykorzystywane tylko do kompilowania i uruchamiania testów.
Najpierw dodaj zależności do swojego projektu (groovy – język który używa Spock oraz samą bibliotekę):
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
<version>4.0.28</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>2.4-groovy-4.0</version>
<scope>test</scope>
</dependency>Ważną zmianą w stosunku do JUnit jest struktura katalogów w projekcie. Zamiast standardowej ścieżki testów napisanych w Javie src/test/java, tutaj zastosujesz osobny katalog: src/test/groovy.
Struktura projektu, który korzysta ze Spock:
src/
├── main/
│ └── java/
│ └── com/
│ └── example/
│ └── Calculator.java
│
└── test/
├── java/
│ └── com/
│ └── example/
│
└── groovy/
└── com/
└── example/
└── CalculatorSpec.groovyPozostaje jeszcze skonfigurowanie kompilacji kodu Groovy. Można do tego wykorzystać GMavenPlus Plugin:
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>4.2.1</version>
<executions>
<execution>
<goals>
<goal>compileTests</goal>
</goals>
</execution>
</executions>
</plugin>GMavenPlus jest tutaj istotny, ponieważ Maven domyślnie kompiluje testy Java z src/test/java, ale nie kompiluje automatycznie plików .groovy znajdujących się w src/test/groovy.
Po takiej konfiguracji mogę umieszczać testy Spock w katalogu src/test/groovy i uruchamiać je razem z pozostałymi testami projektu za pomocą standardowego polecenia Maven: maven test. Warto również stosować charakterystyczną dla Spocka konwencję nazewnictwa klas testowych z końcówką Spec lub Specification, np. CalculatorSpec.groovy. Dzięki temu od razu widać, że dana klasa pochodzi z frameworka Spock.
Pierwszy test jednostkowy w Spock Framework
Po skonfigurowaniu projektu możesz przejść do napisania swojego pierwszego testu. Na potrzeby przykładu napiszę prostą klasę Calculator:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}Aby test został poprawnie zlokalizowany przez bibliotekę, klasa testowa musi rozszerzać klasę Specification oraz zawierać się w src/test/groove:
import spock.lang.Specification
class CalculatorSpec extends Specification {
def "should add two numbers"() {
given:
def calculator = new Calculator()
when:
def result = calculator.add(2, 3)
then:
result == 5
}
}Już na tym prostym przykładzie widać charakterystyczną składnię Spocka. Metoda testowa nie musi posiadać adnotacji @Test, a jej nazwa jest zwykłym tekstem opisującym sprawdzany scenariusz. Każdy test jest podzielony na czytelne bloki określające kolejne etapy jego wykonania.
O to główne z nich:
- Blok
given, który służy do definiowania danych potrzebnych do wykonania testu - Sekcja when gdzie wykonujesz operację, które chcesz przetestować
- oraz then w której sprawdzasz czy zaszły odpowiednie warunki do powodzenia testu
Zwrócić uwagę na brak znanego z JUnit wywołania asercji. W bloku then Spock automatycznie traktuje wyrażenia logiczne jako warunki testu. Jeżeli result == 5 zwróci false, test zakończy się niepowodzeniem.
Najważniejsze bloki w testach Spock
Blok given, który służy do definiowania danych potrzebnych do wykonania testu
Sekcja when gdzie wykonujesz operację, które chcesz przetestować
W then sprawdzasz czy zaszły odpowiednie warunki do powodzenia testu
Czasami możesz skorzystać z krótszego zapisu w swoim teście i pominąć klasyczną strukturę given – when – then i skorzystać z bloku expect. Sekcja ta pozwala połączyć wykonanie operacji oraz sprawdzenie jej rezultatu. Dobrze sprawdza się w przypadku prostych testów, w których rozdzielenie when i then nie poprawiłoby czytelności.
Zamiast:
when: def result = calculator.add(10, 5) then: result == 15
Możesz użyć:
expect: calculator.add(10, 5) == 15
Testy parametryzowane w Spock – blok where
Jedną z największych zalet Spock Framework jest bardzo proste tworzenie testów parametryzowanych, określanych również jako data-driven tests. Zamiast tworzyć kilka niemal identycznych metod testowych dla różnych danych wejściowych, mogę zdefiniować jeden test i uruchomić go wielokrotnie dla różnych zestawów danych za pomocą bloku where.
Wykorzystując wcześniejszą klasę Calculator, test metody add() może wyglądać następująco:
def "should add two numbers"() {
expect:
calculator.add(a, b) == expectedResult
where:
a | b || expectedResult
1 | 2 || 3
2 | 3 || 5
5 | 5 || 10
}Mockowanie zależności w Spock Framework
Spock Framework posiada własny mechanizm mockowania, alternatywny do bibliotek takich jak Mockito. Spock sam pozwala tworzyć mocki, definiować zwracane przez nie wartości oraz sprawdzać liczbę i sposób wywołań metod zależności.
Omówmy tą funkcje na przykładzie poniższego kodu:
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public Order findOrder(Long id) {
return orderRepository.findById(id);
}
}Utworzenie mocka OrderRepository:
def orderRepository = Mock(OrderRepository) def orderService = new OrderService(orderRepository)
Zdefiniowanie wartości zwracanej przez mock:
orderRepository.findById(1L) >> new Order(1L, "Laptop")
Pełen przykład:
def "should return order"() {
given:
def orderRepository = Mock(OrderRepository)
def orderService = new OrderService(orderRepository)
def order = new Order(1L, "Laptop")
orderRepository.findById(1L) >> order
when:
def result = orderService.findOrder(1L)
then:
result == order
}Sprawdzenie, czy metoda została wywołana dokładnie raz:
1 * orderRepository.findById(1L)
Sprawdzenie, czy metoda nie została wywołana – _ oznacza dowolny argument:
0 * orderRepository.delete(_)
Utworzenie Stub:
def orderRepository = Stub(OrderRepository) orderRepository.findById(1L) >> new Order(1L, "Laptop")
Testowanie wyjątków w Spock Framework
Spock pozwala w czytelny sposób sprawdzać, czy dana metoda wyrzuciła oczekiwany wyjątek. Wykorzystuje się do tego metodę thrown(), którą umieszcza się w bloku then.
Przykładowy kod do testowania:
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Divider cannot be zero");
}
return a / b;
}Sprawdzenie typu wyjątku i wiadomości:
when: calculator.divide(10, 0) then: def exception = thrown(IllegalArgumentException) exception.message == "Divider cannot be zero"
Sprawdzenie, że wyjątek nie został rzucony:
when: calculator.divide(10, 2) then: notThrown(IllegalArgumentException)
Spock i Spring Boot
Spock można również wykorzystać do testowania aplikacji opartych na Spring Boot. Integrację Spocka ze Springiem zapewnia moduł spock-spring, który należy dodać do zależności testowych projektu.
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>2.4-groovy-4.0</version>
<scope>test</scope>
</dependency>Załóżmy, że aplikacja posiada prosty serwis napisany w Javie:
@Service
public class CalculatorService {
public int add(int a, int b) {
return a + b;
}
}Prosty test @SpringBootTest:
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
@SpringBootTest
class CalculatorServiceSpec extends Specification {
@Autowired
CalculatorService calculatorService
def "should add two numbers"() {
expect:
calculatorService.add(10, 5) == 15
}
}Mock beana Springa za pomocą @SpringBean:
import org.spockframework.spring.SpringBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
@SpringBootTest
class OrderServiceSpec extends Specification {
@SpringBean
OrderRepository orderRepository = Mock()
@Autowired
OrderService orderService
def "should return order from repository"() {
given:
def order = new Order(1L, "Laptop")
when:
def result = orderService.findOrder(1L)
then:
1 * orderRepository.findById(1L) >> Optional.of(order)
result == order
}
}Wady i zalety Spock
Spock Framework jest ciekawą alternatywą dla klasycznego zestawu JUnit i Mockito, szczególnie gdy zależy mi na czytelnych testach oraz prostym testowaniu wielu zestawów danych. Nie oznacza to jednak, że będzie najlepszym rozwiązaniem w każdym projekcie Java.
| Zalety Spock Framework | Wady Spock Framework |
|---|---|
Czytelna struktura testów dzięki given, when, then, expect | Konieczność znajomości podstaw Groovy |
| Bardzo zwięzła składnia testów | Wprowadzenie dodatkowego języka do projektu Java |
Świetne wsparcie dla testów parametryzowanych przez where | Dodatkowa konfiguracja projektu Maven |
| Wbudowane mocki i stuby | Mniejsza popularność niż JUnit |
| Prosta weryfikacja wywołań metod mocków | Dodatkowy próg wejścia dla programistów znających tylko Javę |
| Czytelne komunikaty przy nieudanych asercjach | Dynamiczne typowanie w Groovy może utrudniać wykrywanie niektórych błędów |
| Dobra integracja ze Spring Boot | Konieczność pilnowania kompatybilności wersji Spock i Groovy |
| Możliwość testowania kodu napisanego w Javie | Mniej naturalny wybór dla projektu, który ma pozostać w 100% javowy |
| Nie wymaga Mockito w typowych przypadkach | JUnit ma większy ekosystem i jest bardziej rozpowszechniony |
Podsumowanie
Spock Framework to ciekawa alternatywa dla klasycznego połączenia JUnit i Mockito, szczególnie w projektach, w których zależy mi na czytelnych i zwięzłych testach. Bloki given, when, then i where, wbudowane mockowanie oraz bardzo wygodne testy parametryzowane pozwalają opisywać nawet bardziej złożone scenariusze w przejrzysty sposób. Spock dobrze współpracuje również z Javą, Mavenem i Spring Boot, dlatego nie wymaga zmiany języka wykorzystywanego w kodzie produkcyjnym. Największym minusem pozostaje konieczność wprowadzenia Groovy do projektu, jednak w zamian otrzymuję narzędzie, które w wielu przypadkach pozwala pisać testy krócej i czytelniej.
