Testowanie REST API z użyciem Spock – Wprowadzenie do Frameworka Spock
W tym artykule chciałbym pokazać, jak przetestować API za pomocą frameworka Spock. Spock to framework testowy dla aplikacji Java i Groovy. Rozszerza on narzędzie JUnit i pozwala nam pisać krótszy i bardziej czytelny kod. Spock wspiera testowanie jednostkowe, BDD (Behavior-driven development), oraz Mocking. Jest również doskonały do testowania sterowanego danymi (Data Driven Testing).
Konfiguracja Spock
Na początek utwórzmy projekt maven i przygotujmy plik pom.xml, dodając do niego niezbędnych zależności.
Wymaganą zależnością do uruchomienia testów Spocka jest spock-core. Dodałem również http-builder, ponieważ będziemy używać RESTClient, który jest częścią tej biblioteki.
<dependencies>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>${spock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.groovy.modules.http-builder</groupId>
<artifactId>http-builder</artifactId>
<version>0.7.2</version>
</dependency>
</dependencies>
Podstawowy przykład
Po dodaniu zależności możemy pisać testy przy użyciu Spock za pomocą języka Groovy. Dla pierwszego przykładu utworzyłem klasę BasicAuthTest.groovy w katalogu src/test/groovy.
Spójrzmy na zawartość klasy:
import groovyx.net.http.HttpResponseException
import groovyx.net.http.RESTClient
import spock.lang.Shared
import spock.lang.Specification
class BasicAuthTest extends Specification {
@Shared
def client = new RESTClient( "$SERVER_URL:$SERVER_PORT")
def 'should return 200 code when used valid credentials' () {
when: 'login with invalid credentials'
client.headers['Authorization'] = "Basic ${"$USERNAME:$PASSWORD".bytes.encodeBase64()}"
def response = client.get( path : '/basic-auth/user/pass' )
then: 'server returns 200 code (ok)'
assert response.status == 200 : 'response code should be 200 when tried to authenticate with valid credentials'
}
def 'should return 401 (unauthorized) code when used invalid credentials' () {
when: 'login with invalid credentials'
client.headers['Authorization'] = "Basic ${"$USERNAME:$INVALID_PASSWORD".bytes.encodeBase64()}"
client.get( path : '/basic-auth/user/pass' )
then: 'server returns 401 code (unauthorized)'
HttpResponseException e = thrown(HttpResponseException)
assert e.response.status == 401: 'response code should be 401 when you use wrong credentials'
}
Każda klasa Spock musi dziedziczyć po interfejsie Specification.
@Shared to adnotacja frameworka Spock, która oznacza, że każda funkcjonalność będzie używać tej samej instancji zmiennej.
Możemy używać ciągów znaków jako nazw metod. Jest to bardzo wygodne, ponieważ w ten sposób uzyskamy czytelne nazwy, które będą następnie wyświetlane w wynikach wykonania testów.
Użyłem słów kluczowych „When” i „Then”. Spock pozwala nam używać stylu BDD, który pozwala używać języka naturalnego do opisu zagadnienia biznesowego, które chcemy rozwiązać.
W sekcji „Given” zazwyczaj dokonujemy pewnych przygotowań, które są niezbędne do uruchomienia naszego testu. „When” to miejsce, gdzie faktycznie uruchamiamy nasze testy i wykonujemy zadane czynności, a w części „Then” (lub „And”) wykonujemy asercje.
Spock pozwala w wygodny sposób na sprawdzanie, czy metoda wywołana w bloku „When” skonczyła się wyjątkiem. Nie musimy używać bloku try-catch. Możemy użyć metod thrown() i notThrown(), aby sprawdzić, czy dany wyjątek został wywołany, czy nie. Dzięki temu możemy też sprawdzić komunikat, odwołując się do tego wyjątku.
Wspomniałem również o przydatności słowa kluczowego assert w Spocku. Zmieńmy oczekiwany kod odpowiedzi w pierwszym teście z „200” na „201” i sprawdźmy wyniki.
Jeśli nasze asercje są skomplikowane lub zajmują dużo linii kodu, możemy je wyodrębnić do metody. Tak długo, jak używamy w niej słowa kluczowego assert, wyniki będą wyświetlane w przyjazny sposób.
Jak widać w poniższym przykładzie, możemy użyć słowa kluczowego „And”, aby dodać dodatkowe kroki. Użyłem go do sprawdzenia kilku dodatkowych asercji.
@Shared
def client = new RESTClient( "$SERVER_URL:$SERVER_PORT")
@Ignore
def 'should return image metadata containing proper fields' () {
when: 'send request for image metadata'
def response = client.get( path : '/photos/1' )
then: 'server returns set of metadata for a single image'
assert response.status == 200 : 'response code should be 200'
assert response.contentType == 'application/json' : 'response should be in json format'
and: 'response contains all required fields'
assert response.data.albumId != null
assert response.data.id != null
assert response.data.title != null
assert response.data.url != null
assert response.data.thumbNailUrl != null
Adnotacja @Ignore oznacza, że specyfikacja (klasa) lub funkcjonalność nie zostanie uruchomiona. Możesz jej użyć, aby pominąć testy, których nie chcesz uruchamiać w danym momencie.
Data Driven Testing – Testowanie sterowane danymi
Wsparcie frameworka Spock dla DDT pozwala nam wielokrotnie wykonywać ten sam kod z różnymi danymi wejściowymi i oczekiwanymi wynikami. Jest to możliwe dzięki Tabelom Danych (Data Tables). Zauważ, że każda Tabela Danych musi mieć co najmniej dwie kolumny. Jeśli potrzebujesz tylko jednej kolumny, druga powinna być wypełniona _ (myślnikami). Myślnik to odpowiednik dowolnej wartości.
def 'should return 400 code (bad request) if provided id parameter does not contain 3 digits' () {
when: 'send request with incorrect id to get album metadata'
client.get( path : '/albums', query : [id : idVal ] )
then: 'server returns 400 code'
HttpResponseException e = thrown(HttpResponseException)
assert e.response.status == 400: 'response code should be 400 if provided incorrect album id parameter'
where:
idVal | _
-1 |
0 |
1 |
15 |
99 |
0001 |
9999 |
99999999999991 |
}
W parametrze „query” możesz podać ciąg zapytania, który jest częścią URL.
Dla pierwszego zapytania URL będzie wyglądać tak: https://jsonplaceholder.typicode.com/albums?id=-1
Teraz spójrz na poniższy przykład wysyłania żądania POST.
@Unroll
def 'should return 201 code (created) when trying to save record with all required fields' () {
when: 'try to save record with all required fields'
def response = client.post(
path: '/photos',
body: [albumId : albumIdVal,
id : idVal,
url : urlVal,
thumbnailUrl: thumbnailUrlVal],
requestContentType : JSON)
client.get( path : '/albums', query : [id : idVal] )
then: 'server returns 201 code (created)'
assert response.status == 201: 'response code should be 201 if provided all required parameters'
where:
albumIdVal | idVal | urlVal | thumbnailUrlVal
141 | 2 | 'http://placehold.it/600/32' | 'http://placehold.it/th/600/32'
101 | 6 | 'http://placehold.it/600/3122' | 'http://placehold.it/th/600/3122'
111 | 56 | 'http://placehold.it/600/23244' | 'http://placehold.it/th/600/23244'
}
Metoda oznaczona adnotacją @Unroll będzie miała swoje iteracje raportowane niezależnie. Zauważ, że nie wpływa to na wykonanie. Zmienia jedynie sposób wyświetlania wyników w raporcie. Jest to przydatne, aby zobaczyć, który przypadek nie powiódł się. Dzięki temu w łatwiejszy sposób możemy znaleźć przyczynę niepowodzenia testu.
Spójrzmy, co zostanie wyświetlone, jeśli jedna z trzech iteracji nie powiedzie się. Poniższy przykład pokazuje różnicę między użyciem adnotacji @Unroll, a jej brakiem.
Bez @Unroll – wszystkie trzy iteracje są połączone w jeden wynik.
Z @Unroll – możemy zobaczyć, która iteracja się nie powiodła.
Podsumowanie
Spock wymaga tylko jednej zależności, czyli spock-core. Spock nie potrzebuje wszystkich asercji jak JUnit do sprawdzania wyników. Używa tylko jednego słowa kluczowego assert, które dostarcza czytelne komunikaty w przypadku gdy warunek nie jest spełniony. Spock używa również składni w stylu Gherkin do definiowania kroków (Given, When, Then). Testy Spocka są pisane w Groovy, który jest językiem skryptowym opartym na JVM ze składnią podobną do Javy. Świetną cechą Groovy jest to, że można również pisać kod w Javie i w większości przypadków zostanie on skompilowany. Będzie działał poprawnie. Dla niektórych osób główną wadą używania Spocka jest to, że wymaga on Groovy. Jeśli Twój projekt korzysta z Javy, będziesz potrzebował dodatkowej zależności dla Groovy. Ponadto składnia może wyglądać dziwnie, jeśli jesteś przyzwyczajony do testów w JUnit.