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.

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami