JAX-RS RESTful Web Service tesztelése

Posted by | No Tags | Szoftverfejlesztés | Nincs hozzászólás a(z) JAX-RS RESTful Web Service tesztelése bejegyzéshez

Ha Java-ban RESTful Web Service-t akarunk készíteni, akkor ehhez a JAX-RS a hivatalosan elfogadott Java API. Ezt a JAX-WS (Java API for XML Web Services) pehelysúlyú kistestvéreként is szokták emlegetni. Jelenleg a JAX-RS 2.0 verzió a legfrissebb (JSR 339-es szabvány), ez 2013. májusában jött ki. A JAX-RS definiálja a RESTful API-t szerver oldalon és a JAX-RS 2.0 óta egy kliens API-t is tartalmaz, ami igen hasznos a teszteléshez. Néhány fontosabb JAX-RS implementáció:

A JAX-RS teszteléshez szükség van egy elérhető szolgáltatásra a kiszolgáló oldalon és az azt hívó kliensre. Automatizált tesztek esetén célszerű, ha a teszt maga gondoskodik róla, hogy a szolgáltatás elérhető legyen. Ehhez a tesztben el kell indítani egy szervert és fel kell rá telepíteni a tesztelendő JAX-RS szolgáltatást. Ebben az esetben már integrációs tesztről beszélhetünk. Nálunk a teszt egy Glassfish-t indított el és arra telepítettük fel a tesztelendő JAX-RS webalkalmazást, mindezt Arquillian segítségével. Ha már elérhető a szolgáltatás, akkor a kliens oldalon egy JUnit tesztbe ágyazva végezhetjük a tesztelést.

Könyvesbolt teszteset

Példának vegyünk egy egyszerű könyvesbolt alkalmazást, ahol a könyveket a címűk alapján kérjük le. Fogadjon most a szerver POST kéréseket, a kérés és a válasz formátuma legyen JSON vagy XML. Így a JAX-RS kód szerver oldali része ehhez hasonló:

@Path("book")
public class BookService {
  BookDao bookDao;
  @Path("by_title")
  @POST
  @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
  @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
  public Book getBook(BookRequest request) {
    return bookDao.getBookByTitle(request.getTitle());
  }
}

A könyvesbolt tartalmazza az alábbi könyveket:

Id Title Author Price
1 War and peace Leo Tolstoy 49
2 East of Eden John Steinbeck 59

Tesztelés JAX-RS client API-val

Mint már említettem a JAX-RS 2.0 már tartalmaz kliens API-t is, ezért először ennek segítségével írjuk meg a teszt kliensünket. A kliens API a javax.ws.rs.client package alatt található. Egy REST híváshoz először a ClientBuilder-rel csinálunk egy Client-et. Ezután a kliens-sel megcélozzuk az URI-val azonosított WebTarget-et, amit aztán felparaméterezünk és elküldjük rá a kérést, POST esetén például egy Entity-be csomagolva. A Response-ból már csak vissza kell olvasnunk a választ egy Book objektumba. A Book és BookRequest sima POJO-k, csak az @XmlRootElement annotációval vannak ellátva a XML/JSON szerializálás miatt. A szerializáláshoz meg kell adnunk egy XML/JSON provider-t, ez most a JacksonJaxbJsonProvider.

import com.wcs.rest.model.Book;
import com.wcs.rest.model.BookRequest;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider;
import org.junit.Test;
import static org.junit.Assert.*;

public class BookTest {

    @Test
    public void testBookByTitle() {
        BookRequest request = new BookRequest();
        request.setTitle("East of Eden");

        Entity entity = Entity.json(request);

        Client client = createClient();

        WebTarget target = client
                .target("http://localhost:8080/rest-service/rest/book/by_title");

        Response response = target
                .request(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .post(entity);

        Book book = response.readEntity(Book.class);

        assertEquals(Status.OK.getStatusCode(), response.getStatus());
        assertEquals(2, book.getId());
        assertEquals("John Steinbeck", book.getAuthor());
        assertEquals(59, book.getPrice());
    }

    Client createClient() {
        return ClientBuilder
                .newBuilder()
                .register(JacksonJaxbJsonProvider.class)
                .build();
    }
}

A kódban az API oldaláról egyszerűnek tűnik minden, azonban ahhoz hogy ez működjön is, még fel kell használni a kliens oldalon valamelyik a JAX-RS Client API implementációt is. Szerver oldalon a JAX-RS implementáció nem szokott probléma lenni, mert szerencsés esetben az már része a szervernek. Itt választhatunk például a már említett három implementáció közül. Ha Maven-es kliens alkalmazásról van szó, akkor az alábbi függőségek tartoznak az egyes implementációkhoz:
Jersey esetén ezek voltak a függőségek:

            org.glassfish.jersey.core
            jersey-client
            2.4.1
            test

            org.glassfish.jersey.media
            jersey-media-json-jackson
            2.4.1
            test

RESTEasy esetén a használt függőségek:


            org.jboss.resteasy
            resteasy-jackson-provider
            3.0.6.Final
            test

            org.jboss.resteasy
            resteasy-client
            3.0.6.Final
            test

Apache CXF esetén a felhasznált függőségek:


            org.apache.cxf
            cxf-rt-rs-client
            3.0.0-milestone1

            org.codehaus.jackson
            jackson-jaxrs
            1.9.13

            org.codehaus.jackson
            jackson-xc
            1.9.12

Tesztelés REST-Assured segítségével

Ha nem a JAX-RS Client API-t akarjuk használni, akkor egy jó alternatíva lehet a REST-Assured. Ez nagyon jól használható és könnyen olvasható fluent API-t biztosít a teszteléshez:

import static com.jayway.restassured.RestAssured.*;
import com.jayway.restassured.http.ContentType;
import com.jayway.restassured.response.Response;
import com.wcs.rest.model.Book;
import com.wcs.rest.model.BookRequest;
import javax.ws.rs.core.Response.Status;
import static org.junit.Assert.*;
import org.junit.Test;

public class BookTest {

    @Test
    public void testGetBookByTitle() {
        BookRequest request = new BookRequest();
        request.setTitle("War and peace");

        Response response = given()
                                .body(request)
                                .contentType(ContentType.JSON)
                            .expect()
                                .contentType(ContentType.JSON)
                                .statusCode(Status.OK.getStatusCode())
                            .when()
                                .post("http://localhost:8080/rest-service/rest/book/by_title");

        Book book = response.as(Book.class);

        assertEquals(1, book.getId());
        assertEquals("Leo Tolstoy", book.getAuthor());
    }
}

A REST hívások logolását nagyon jól megoldották a REST-Assured-ben. Egyrészt könnyen konfigurálható mind a kérés, mind a válasz esetén a logolás. Másrészt szépen formázott JSON-t illetve XML-t láthatunk a logban. Ezek mind nagyon hasznosak a tesztelésnél. Az alábbi kódrészletben a kérésnél mindent logolunk, a válasznál csak a body-t.

given()
  .body(request)
  .contentType(ContentType.JSON)
  .log().all()
.expect()
  .contentType(ContentType.JSON)
  .statusCode(Status.OK.getStatusCode())
  .log().body()
.when()
  .post("http://localhost:8080/rest-service/rest/book/by_title");

Integrációs teszt Arquillian segítségével

Korábban már említettem, hogy az automatizált tesztekhez az szükséges, hogy a tesztelendő JAX-RS szolgáltatást maga a teszt telepítse fel mielőtt a teszt hívások lefutnának. Az eddig bemutatott példák erről nem gondoskodtak. Most röviden bemutatom, hogy Arquillian segítségével hogyan tudunk ilyen integrációs tesztet készíteni. Az Arquillian részletes paraméterezésére (pl. arquillian.xml, Maven függőségek) most nem térek ki, erre vonatkozó dokumentáció megtalálható például itt. Az integrációs teszt kódja:

import static com.jayway.restassured.RestAssured.*;
import com.jayway.restassured.http.ContentType;
import com.wcs.rest.dao.BookDao;
import com.wcs.rest.model.Book;
import com.wcs.rest.model.BookRequest;
import com.wcs.rest.service.BookService;
import java.net.URI;
import java.net.URL;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(Arquillian.class)
public class BookIT {

    @ArquillianResource
    URL deploymentUrl;

    @Deployment
    public static WebArchive create() {
        return ShrinkWrap.create(WebArchive.class, "rest-service.war")
                .addClasses(BookService.class,
                        BookDao.class,
                        Book.class,
                        BookRequest.class,
                        ...); // classes and other resources into the war
    }

    @Test
    public void testGetBookByTitle() {
        given()
            .body(new BookRequest("War and peace"))
            .contentType(ContentType.JSON)
        .expect()
            .contentType(ContentType.JSON)
            .statusCode(Status.OK.getStatusCode())
        .when()
            .post(buildUri("rest", "book", "by_title"));
    }

    URI buildUri(String... paths) {
        UriBuilder builder = UriBuilder.fromUri(deploymentUrl.toString());
        for (String path : paths) {
            builder.path(path);
        }
        return builder.build();
    }
}

Ez első ránézésre egy sima JUnit teszt. Attól lesz belőle integrációs teszt, hogy a @RunWith(Arquillian.class) annotációval megadjuk, hogy a teszt az Arquillian-nal fusson. A @Deployment annotációval ellátott metódusban rakjuk össze a telepítendő webalkalmazást (WAR-t). A Shrinkwrap-es WebArchive-ba bele kell tennünk az alkalmazás által használt osztályokat, JAR-okat és egyéb erőforrásokat (pl. web.xml). Az Arquillian ezt a WAR-t fogja feltelepíteni a konfigurációban (arquillian.xml) beállított konténerre (pl. Glassfish vagy egyéb Java EE alkalmazásszerver). A JUnit tesztek azután fognak lefutni, hogy ez a telepítés megtörtént. Érdemes még megemlíteni az @ArquillianResource annotációval ellátott URL-t. Az Arquillian ide a feltelepített alkalmazás URL-jét fogja betenni, így azt nem kell beégetnünk a tesztekbe. Ez azért hasznos, mert ha több különböző konténerrel is futtatni akarjuk a tesztjeinket, akkor nem kell azzal vesződni, hogy ezek ugyanazt a host, ip, stb. beállításokat használják, hiszen ebbe a változóba futásidőben bekerül a helyes URL.


No Comments

Leave a comment