5. Api cliente. Procesamiento JSON y Pruebas

Hasta ahora hemos hablado sobre la creación de servicios web RESTful y hemos "probado" nuestros servicios utilizando el cliente que nos proporciona IntelliJ, curl, o Postman, para realizar peticiones y observar las respuestas. JAX-RS 2.0 proporciona un API cliente para facilitar la programación de clientes REST, que presentaremos en esta sesión.

En sesiones anteriores, hemos trabajado con representaciones de texto y xml, fundamentalmente. Aquí hablaremos con más detalle de JSON, que constituye otra forma de representar los datos de las peticiones y respuestas de servicios REST muy extendida.

Finalmente, veremos cómo implementar pruebas sobre nuestro servicio utilizando el API cliente y el framework junit.

5.1. API cliente. Visión general

La especificación JAX-RS 2.0 incorpora un API cliente HTTP, que facilita enormemente la implementación de nuestros clientes RESTful, y constituye una clara alternativa al uso de clases Java como java.net.URL, librerías externas (como la de Apache) u otras soluciones propietarias.

El API cliente está contenido en el paquete javax.ws.rs.client, y está diseñado para que se pueda utilizar de forma "fluida" (fluent). Esto significa, como ya hemos visto, que lo utilizaremos "encadenando" una sucesión de llamadas a métodos del API, permitiéndonos así escribir menos líneas de código. Básicamente está formado por tres clases principales: Client, WebTarget y Response (ya hemos hablado de esta última en sesiones anteriores).

Para acceder a un recurso REST mediante el API cliente es necesario seguir los siguientes pasos:

  1. Obtener una instancia de la interfaz Client

  2. Configurar la instancia Client a través de un target (instancia de WebTarget)

  3. Crear una petición basada en el target anterior

  4. Invocar la petición

Vamos a mostrar un código ejemplo para ilustrar los pasos anteriores. En este caso vamos a invocar peticiones POST y GET sobre una URL (target) para crear y posteriormente consultar un objeto Cliente que representaremos en formato XML:

...
Client client = ClientBuilder.newClient(); (1)

WebTarget target =
      client.target("http://expertojava.org/clientes"); (2)

Response response =
      target
        .request()  (3)
        .post(Entity.xml(new Cliente("Alvaro", "Gomez"))); (4)

response.close(); (5)

Cliente cliente = target.queryParam("nombre", "Alvaro Gomez")
                        .request()
                        .get(Cliente.class);  (6)
client.close();
...
1 Obtenemos una instancia javax.ws.rs.client.Client
2 Creamos un WebTarget
3 Creamos la petición
4 Realizamos una invocación POST
5 Cerramos (liberamos) el input stream para esta respuesta (en el caso de que esté disponible y abierto). Es una operación idempotente, es decir, podemos invocarla múltiples veces con el mismo efecto
6 A partir de un WebTarget, establecemos los valores de los queryParams de la URI de la petición, creamos la petición, y realizamos una invocación GET

A continuación explicaremos con detalle los pasos a seguir para implementar un cliente utilizando el API de JAX-RS.

5.1.1. Obtenemos una instancia Client

La interfaz javax.ws.rs.client.Client es el principal punto de entrada del API Cliente. Dicha interfaz define las acciones e infraestructura necesarias requeridas por un cliente REST para "consumir" un servicio web RESTful. Los objetos Client se crean a partir de la clase ClientBuilder:

Clase ClientBuilder: utilizada para crear objetos Client
public abstract class ClientBuilder implements Configurable<ClientBuilder> {
  public static Client newClient() {...}
  public static Client newClient(final Configuration configuration) {...}

  public static ClientBuilder newBuilder() {...}

  public abstract ClientBuilder sslContext(final SSLContext sslContext);
  public abstract ClientBuilder keyStore(final KeyStore keyStore,
                                          final char[] password);
  public ClientBuilder keyStore(final KeyStore keyStore,
                                final String password) {}
  public abstract ClientBuilder trustStore(final KeyStore trustStore);
  public abstract ClientBuilder hostnameVerifier(final HostnameVerifier verifier);

  public abstract Client build();
}

La forma más sencilla de crear un objeto Client es mediante ClientBuilder.newClient(). Este método proporciona una instancia pre-inicializada de tipo Client lista para ser usada. La clase ClientBuilder nos proporciona métodos adicionales, con los que podremos configurar diferentes propiedades del objeto.

Veamos un ejemplo de uso de ClientBuilder.newBuilder() utilizando además alguno de los métodos proporcionados para configurar nuestra instancia de tipo Client que vamos a crear. Los métodos register() y property() son métodos de la interfaz Configurable (y que son implementados por ClientBuilder).

Ejemplo de uso de ClientBuilder
Client cliente = ClientBuilder.newBuilder() (1)
                      .property("connection.timeout", 100) (2)
                      .sslContext(sslContext) (3)
                      .register(JacksonJsonProvider.class) (4)
                      .build(); (5)
1 Creamos un ClientBuilder invocando al método estático ClientBuilder.newBuilder()
2 Asignamos una propiedad específica de la implementación concreta de JAX-RS que estemos utilizando que controla el timeout de las conexiones de los sockets
3 Especificamos el sslContext que queremos utilizar para gestionar las conexiones HTTP
4 Registramos a través del método register() (de la interfaz Configurable) una clase anotada con @Provider. Dicha clase "conoce" cómo serializar objetos Java a JSON y viceversa
5 Finalmente, realizamos una llamada a build() para crear la instancia Client

Las instancias de Client gestionan conexiones con el cliente utilizando sockets y son objetos bastante pesados. Se deberían reutilizar las instancias de esta interfaz en la medida de lo posible, ya que la inicialización y destrucción de dichas instancias consume mucho tiempo. Por lo tanto, por razones de rendimiento, debemos limitar el número de instancias Client en nuestra aplicación.

Client client = ClientBuilder.newClient(); (1)
...
client.close(); (2)
1 Obtenemos una instancia de tipo Client invocando al método ClientBuilder.newClient()
2 Utilizamos el método close() para "cerrar" la instancia Client después de realizar todas las invocaciones sobre el target del recurso. De esta forma, "cerramos" la conexión de forma que se liberan sus recursos, y ya no podremos seguir usándola.

Recuerda siempre invocar el método close() sobre nuestros objetos Client después de que hayamos realizado todas las invocaciones sobre el target del/los recurso/s REST. A menudo, los objetos Client reutilizan conexiones por razones de rendimiento. Si no los cerramos después de utilizarlos, estaremos desaprovechando recursos del sistema muy "valiosos". Cerrar la conexión implica cerrar el socket

Igualmente, si el resultado de una invocación a un servicio rest es una instancia de Response debemos invocar el método close() sobre dichos objetos Response para liberar la conexión. Liberar una conexion significa permitir que ésta esté disponible para otro uso por una instancia Client. Liberar la conexión no implica cerrar el socket.

La interfaz Client es una sub-interfaz de Configurable. Esto nos permitirá utililizar los métodos property() y register() para cambiar la configuración y registrar componentes en la parte del cliente en tiempo de ejecución.

Interfaz Client (es una subinterfaz de Configurable)
public interface Client extends Configurable<Client> {

   public void close();

   public WebTarget target(String uri);
   public WebTarget target(URI uri);
   public WebTarget target(UriBuilder uriBuilder);
   public WebTarget target(Link link);
   ...
}

Sin embargo, el principal propósito de Client es crear instancias de WebTarget, como veremos a continuación.

5.1.2. Configuramos el target del cliente (URI)

La interfaz javax.ws.rs.client.WebTarget representa la URI específica que queremos invocar para acceder a un recurso REST particular.

Interfaz WebTarget (es una subinterfaz de Configurable)
public interface WebTarget extends Configurable<WebTarget> {

  public URI getUri();
  public UriBuilder getUriBuilder();

  public WebTarget path(String path);
  public WebTarget resolveTemplate(String name, Object value);
  ...
  public WebTarget resolveTemplates(Map<String, Object> templateValues);
  ...
  public WebTarget matrixParam(String name, Object... values);
  public WebTarget queryParam(String name, Object... values);
  ...
}

La interfaz WebTarget tiene métodos para extender la URI inicial que hayamos construido. Podemos añadir, por ejemplo, segmentos de path o parámetros de consulta invocando a los métodos WebTarget.path(), o WebTarget.queryParam(), respectivamente. Si la instancia de WebTarget contiene plantillas de parámetros, los métodos WebTarget.resolveTemplate() pueden asignar valores a las variables correspondientes. Por ejemplo:

WebTarget target =
        client
           .target("http://ejemplo.com/clientes/{id}") (1)
           .resolveTemplate("id", "123") (2)
           .queryParam("verboso", true); (3)
1 Inicializamos un WebTarget con una URI que contiene una plantilla con un parámetro: {id}. El objeto client es una instancia de la clase `Client
2 El método resolveTemplate() "rellena" la expresión id con el valor "123"
3 Finalmente añadimos a la URI un parámetro de consulta: ?verboso=true

Las instancias de WebTarget son inmutables con respecto a la URI que contienen. Esto significa que los métodos para especificar segmentos de path adicionales y parámetros devuelven una nueva instancia de WebTarget. Sin embargo, las instancias de WebTarget son mutables respecto a su configuración. Por lo tanto, la configuración de objetos WebTarget no crea nuevas instancias.

Veamos otro ejemplo:

WebTarget base = cliente.target("http://expertojava.org/"); (1)
WebTarget clienteURI = base.path("cliente"); (2)
clienteURI.register(MyProvider.class); (3)
1 base es una instancia de WebTarget con el valor de URI http://exertojava.org/
2 clienteURI es una instancia de WebTarget con el valor de URI http://exertojava.org/cliente
3 Configuramos clienteURI registrando la clase MyProvider

En este ejemplo creamos dos instancias de WebTarget. La instancia clienteURI hereda la configuración de base y posteriormente modificamos la configuramos registrando una clase Provider. Los cambios sobre la configuración de clienteURI no afectan a la configuración de base, ni tampoco se crea una nueva instancia de WebTarget.

Los beneficios del uso de WebTarget se hacen evidentes cuando construimos URIs complejas, por ejemplo cuando extendemos nuestra URI base con segmentos de path adicionales o plantillas. El siguiente ejemplo ilustra estas situaciones:

WebTarget base = cliente.target("http://expertojava.org/"); (1)
WebTarget saludo = base.path("hola").path("{quien}"); (2)
Response res = saludo.resolveTemplate("quien", "mundo").request().get();
1 base representa la URI: http://expertojava.org
2 saludo representa la URI: http://expertojava/hola/{quien}

En el siguiente ejemplo, utilizamos una URI base, y a partir de ella construimos otras URIs que representan servicios diferentes proporcionados por nuestro recurso REST.

Client cli = ClientBuilder.newClient();
WebTarget base = client.target("http://ejemplo/webapi");
WebTarget lectura = base.path("leer"); (1)
WebTarget escritura = base.path("escribir"); (2)
1 lectura representa la uri: http://ejemplo/webapi/leer
2 escritura representa la uri: http://ejemplo/webapi/escribir

El método WebTarget.path() crea una nueva instancia de WebTarget añadiendo a la URI actual el segmento de ruta que se pasa como parámetro.

5.1.3. Construimos y Realizamos la petición

Una vez que hemos creado y configurado convenientemente el WebTarget, que representa la URI que queremos invocar, tenemos que construir la petición y finalmente realizarla.

Para construir la petición podemos Utilizar uno de los métodos WebTarget.request(), que mostramos a continuación:

Interfaz WebTarget: métodos para comenzar a construir la petición
public interface WebTarget extends Configurable<WebTarget> {
  ...
  public Invocation.Builder request();
  public Invocation.Builder request(String... acceptedResponseTypes);
  public Invocation.Builder request(MediaType... acceptedResponseTypes);
}

Normalmente invocaremos WebTarget.request() pasando como parámetro el media type aceptado como respuesta, en forma de String o utilizando una de las constantes de javax.ws.rs.core.MediaType. Los métodos WebTarget.request() devuelven una instancia de Invocation.Builder, una interfaz que proporciona métodos para preparar la petición del cliente y también para invocarla.

La interface Invocation.Builder Contiene un conjunto de métodos que nos permiten construir diferentes tipos de cabeceras de peticiones. Así, por ejemplo, proporciona varios métodos acceptXXX() para indicar diferentes tipos MIME, lenguajes, o "encoding" aceptados. También proporciona métodos cookie() para especificar cookies para enviar al servidor. Finalmente proporciona métodos header() para especificar diferentes valores de cabeceras.

Ejemplos de uso de esta interfaz para construir la petición:

Client cli = ClientBuilder.newClient();
cli.invocation(Link.valueOf("http://ejemplo/rest")).accept("application/json").get();
//si no utilizamos el método invocation, podemos hacerlo así:
cli.target("http://ejemplo/rest").request("application/json").get();
Client cliente = ClientBuilder.newClient();
WebTarget miRecurso = client.target("http://ejemplo/webapi/mensaje")
 .request(MediaType.TEXT_PLAIN);

El uso de una constante MediaType es equivalente a utilizar el String que define el tipo MIME:

Invocation.Builder builder = miRecurso.request("text/plain");

Hemos visto que WebTarget implementa métodos request() cuyos parámetros especifican el tipo MIME de la cabecera Accept de la petición. El código puede resultar más "legible" si usamos en su lugar el método Invocation.Builder.accept(). En cualquier caso es una cuestión de gustos personales.

Después de determinar el media type de la respuesta, invocamos la petición realizando una llamada a uno de los métodos de la instancia de Invocation.Builder que se corresponde con el tipo de petición HTTP esperado por el recurso REST, al que va dirigido dicha petición. Estos métodos son:

  • get()

  • post()

  • delete()

  • put()

  • head()

  • options()

La interfaz Invocation.Builder es una subinterfaz de la interfaz SyncInvoker, y es la que especifica los métodos anteriores (get, post, …​) para realizar peticiones síncronas, es decir, que hasta que no nos conteste el servidor, no podremos continuar procesando el código en la parte del cliente.

Las peticiones GET tienen los siguientes prototipos:

Interface SyncInvoker: peticiones GET síncronas
public interface SyncInvoker {
  ...
  <T> T get(Class<T> responseType);
  <T> T get(GenericType<T> responseType);
  Response get();
  ...
}

Los primeros dos métodos genéricos convertirán una respuesta HTTP con éxito a tipos Java específicos indicados como parámetros del método. El tercero devuelve una instancia de tipo Response. Por ejemplo:

Ejemplos de peticiones GET utilizando el API cliente jasx-rx 2.0
Client cli = ClientBuilder.newClient();

//petición get que devuelve una instancia de Cliente
Cliente cliRespuesta = cli.target("http://ejemplo/clientes/123")
                          .request("application/json")
                          .get(Cliente.class); (1)

//petición get que devuelve una lista de objetos Cliente
List<Cliente> cliRespuesta2 =
                 cli.target("http://ejemplo/clientes")
                    .request("application/xml")
                    .get(new GenericType<List<Cliente>>() {}); (2)

//petición get que devuelve un objeto de tipo Response
Response respuesta =
              cli.target("http://ejemplo/clientes/245")
                 .request("application/json")
                 .get();  (3)
try {
  if (respuesta.getStatus() == 200) {
     Cliente cliRespuesta =
                respuesta.readEntity(Cliente.class); (4)
  }
} finally {
    respuesta.close();
}
1 En la primera petición queremos que el servidor nos devuelva la respuesta en formato JSON, y posteriormente la convertiremos en el tipo Cliente utilizando un de los componentes MessageBodyReader registrados.
2 En la segunda petición utilizamos la clase javax.ws.rs.core.GenericType para informar al correspondiente MessageBodyReader del tipo de objetos de nuestra Lista. Para ello creamos una clase anónima a la que le pasamos como parámetro el tipo genérico que queremos obtener.
3 En la tercera petición obtenemos una instancia de Response, a partir de la cual podemos obtener el cuerpo del mensaje de respuesta del servidor
4 El método readEntity() asocia el tipo Java solicitado (en este caso el tipo java Cliente) y el contenido de la respuesta recibida con el correspondiente proveedor de entidades (de tipo MessageBodyReader) para obtener dicho tipo Java a partir de la respuesta HTTP recibida.  En sesiones anteriores hemos utilizado la clase Response desde el servicio REST, para construir la respuesta que se envía al cliente.

Recordemos algunos de los métodos que podemos utilizar desde el cliente para analizar la respuesta que hemos obtenido:

Métodos de la clase Response que utilizaremos desde el cliente
public abstract class Response {
  public abstract Object getEntity();  (1)
  public abstract int getStatus();  (2)
  public abstract Response.StatusType getStatusInfo() (3)
  public abstract MultivaluedMap<String, Object> getMetadata();  (4)
  public abstract URI getLocation();  (5)
  public abstract MediaType getMediaType();  (6)
  public MultivaluedMap<String,Object> getHeaders(); (7)
  public abstract <T> T readEntity(Class<T> entityType); (8)
  public abstract <T> T readEntity(GenericType<T> entityType); (9)
  public abstract void close(); (10)
   ...
}
1 El método getEntity() devuelve el objeto Java correspondiente al cuerpo del mensaje HTTP.
2 El método getStatus() devuelve el código de respuesta HTTP.
3 El método getStatusInfo() devuelve la información de estado asociada con la respuesta.
4 El método getMetadata() devuelve una instancia de tipo MultivaluedMap con las cabeceras de la respuesta.
5 El método getLocation() devuelve la URI de la cabecera Location de la respuesta.
6 El método getMediaType() devuelve el mediaType del cuerpo de la respuesta
7 El método getHeaders() devuelve las cabeceras de respuesta con sus valores correspondientes.
8 El método readEntity() devuelve la entidad del cuerpo del mensaje utilizando un MessageBodyReader que soporte el mapeado del inputStream de la entidad a la clase Java especificada como parámetro.
9 El método readEntity() también puede devolver una clase genérica si se dispone del MessageBodyReader correspondiente.
10 El método close() cierra el input stream correspondiente a la entidad asociada del cuerpo del mensaje (en el caso de que esté disponible y "abierto"). También libera cualquier otro recurso asociado con la respuesta (como por ejemplo datos posiblemente almacenados en un buffer).

Veamos otro ejemmplo. Si el recurso REST espera una petición HTTP GET, invocaremos el método Invocation.Builder.get(). El tipo de retorno del método debería corresponderse con la entidad devuelta por el recurso REST que atenderá la petición.

Client cliente = ClientBuilder.newClient();
WebTarget miRecurso = cliente.target("http://ejemplo/webapi/lectura");
String respuesta = miRecurso.request(MediaType.TEXT_PLAIN)
                            .get(String.class);

O también podríamos codificarlo como:

Client cliente = ClientBuilder.newClient();
String respuesta = cliente
                     .target("http://ejemplo/webapi/lectura")
                     .request(MediaType.TEXT_PLAIN)
                     .get(String.class);

Si el tipo de retorno de la petición GET es una colección, usaremos javax.ws.rs.core.GenericType<T> como parámetro del método, en donde T es el tipo de la colección:

List<PedidoAlmacen> pedidos = client
                              .target("http://ejemplo/webapi/lectura")
                              .path("pedidos")
                              .request(MediaType.APPLICATION_XML)
                              .get(new GenericType<List<PedidoAlmacen>>() {});

Si el recurso REST destinatario de la petición espera una petición de tipo HTTP POST, invocaremos el método Invocation.Builder.post().

Las peticiones POST tienen los siguientes prototipos:

Interface SyncInvoker: peticiones POST síncronas
public interface SyncInvoker {
  ...
  <T> T post(Entity<?> entity, Class<T> responseType);
  <T> T post(Entity<?> entity, GenericType<T> responseType)
  Response post(Entity<?> entity);
  ...
}

Los primeros dos métodos genéricos envían una entidad (clase java + tipo MIME asociado), indicada como primer parámetro del método, y como segundo parámetro se indica el tipo java al que se convertirá la respuesta recibida. El tercero envía una entidad y devuelve una instancia de tipo Response. Por ejemplo:

Veamos un ejemplo de invocación de peticiones POST.

Ejemplo de petición POST utilizando el API cliente jasx-rx 2.0
Client cli = ClientBuilder.newClient();
Pedido pe = new PedidoAlmacen(...);
Pedido peRespuesta = cli
            .target(...)
            .request()
            .post(Entity.entity(new Pedido(), "application/json"),
                  Pedido.class);

En este caso estamos realizando una petición POST. Como payload del mensaje enviamos un objeto Pedido representado en formato json. La entidad esperada como respuesta debe ser de tipo Pedido.

Esto implica que en el lado del servidor, el método que atiende la petición @Post tendrá un parámetro de tipo Pedido y se deberán serializar los objetos de tipo Pedido a json, ya que es el tipo MIME asociado a esta entidad ( especificado en la cabera Content-Type de la petición HTTP).

La clase Entity encapsula los objetos Java que queremos enviar con las peticiones GET o POST. No tiene un constructor público. En su lugar tenemos que invocar uno de sus métodos estáticos:

Clase javax.ws.rs.client.Entity
public final class Entity<T> {
   ...
   public static <T> Entity<T> entity(T entity, String mediaType) (1)
   public static <T> Entity<T> entity(T entity, MediaType mediaType) (2)
   public static <T> Entity<T> xml(final T entity) { } (3)
   public static <T> Entity<T> json(final T entity) { } (4)
   public static <T> Entity<T> text(T entity) { } (5)
   public static Entity<Form> form(final Form form) { } (6)
   ...
}
1 El método estático entity() crea una entidad (clase Java) con un tipo MIME asociado dado por la cadena de caracteres mediaType
2 El método estático entity() crea una entidad (clase Java) con un tipo MIME indicado en mediaType
3 El método xml crea una entidad (clase Java) con el tipo MIME "application/xml"
4 El método json crea una entidad (clase Java) con el tipo MIME "application/jsom"
5 El método text crea una entidad (clase Java) con el tipo MIME "text/plain"
6 El método form crea una entidad (clase Java) con el tipo MIME "application/x-www-form-urlencoded"

Veamos otro ejemplo de invocación POST que utiliza la clase Entity:

Ejemplo de petición POST y uso de clase Entity
NumSeguimiento numSeg = client
          .target("http://ejemplo/webapi/escritura")
          .request(MediaType.APPLICATION_XML)  (1)
          .post(Entity.xml(pedido), NumeroSeguimiento.class); (2)
1 Especificamos como parámetro de la petición request() el tipo MIME que aceptamos en la respuesta (cabecera HTTP Accept).
2 Realizamos una petición POST. El cuerpo del mensaje se crea con la llamada Entity.xml(pedido). El tipo Entity encapsula la entidad del mensaje (tipo Java Pedido) y el tipo MIME asociado (tipo MIME application/xml) .

Veamos un ejemplo en el que enviamos parámetros de un formulario en una petición POST:

Ejemplo de envío de datos de un formulario en una petición POST
Form form = new Form().param("nombre", "Pedro")
                      .param("apellido", "Garcia");
...
Response response = client.target("http://ejemplo/clientes")
                    .request().
                    .post(Entity.form(form));
response.close();

La petición POST del código anterior envía los datos del formulario, y espera recibir como respuesta una entidad de tipo Response.

El código en el lado del servidor será similar a éste:

Servicio rest que sirve una petición POST a partir de datos de un formulario
...
@POST
@Path("/clientes")
@Produces("text/html")
public Response crearCliente(@FormParam("nombre")String nom,
            @FormParam("apellido")String ape)
{
    ... //creamos el nuevo cliente
    return Response.ok(RESPONSE_OK).build();
}

5.1.4. Manejo de excepciones

Veamos qué ocurre si se produce una excepción cuando utilizamos una forma de invocación que automáticamente convierte la respuesta en el tipo especificado. Supongamos el siguiente ejemplo:

Cliente cli = client.target("http://tienda.com/clientes/123")
                    .request("application/json")
                    .get(Cliente.class);

En este escenario, el framework del cliente convierte cualquier código de error HTTP en una de las excepciones que añade JAX-RS 2.0 (BadRequesException, ForbiddenException…​) y que ya hemos visto. Podemos capturar dichas excepciones en nuestro código para tratarlas adecuadamente:

try {
  Cliente cli = client.target("http://tienda.com/clientes/123")
                      .request("application/json")
                      .get(Cliente.class);
} catch (NotAcceptableException notAcceptable) {
      ...
} catch (NotFoundException notFound) {
      ...
}

Si el servidor responde con un error HTTP no cubierto por alguna excepción específica JAX-RS, entonces se lanza una excepción de propósito general. La clase ClientErrorException cubre cualquier código de error en la franja del 400. La clase ServerErrorException cubre cualquier código de error en la franja del 500.

Si el servidor envía alguna de los códigos de respuesta HTTP 3xx (clasificados como códigos de la categoría redirección), el API cliente lanza una RedirectionException, a partir de la cual podemos obtener la URL para poder tratar la redirección nosotros mismos. Por ejemplo:

WebTarget target = client.target("http://tienda.com/clientes/123");
boolean redirected = false;

Cliente cli = null;
do {
     try {
          cli = target.request("application/json")
                      .get(Cliente.class);
     } catch (RedirectionException redirect) {
          if (redirected) throw redirect;
          redirected = true;
          target = client.target(redirect.getLocation());
   }
} while (cli == null);

En este ejemplo, volvemos a iterar si recibimos un código de respuesta 3xx. El código se asegura de que sólo permitimos un código de este tipo, cambiando el valor de la variable redirect en el bloque en el que capturamos la exceptión. A continuación cambiamos el WebTarget (en el bloque catch) al valor de la cabecera Location de la respuesta del servidor.

Los códigos de estado HTTP 3xx indican que es neceario realizar alguna acción adicional para que el servidor pueda completar la petición. La acción requerida puede llevarse a cabo sin necesidad de interactuar con el cliente sólo si el método utilizado en la segunda petición es GET o HEAD (ver http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)

5.2. Procesamiento JSON

JSON (JavaScript Object Notation) es un formato para el intercambio de datos basado en texto, derivado de Javascript (Javascript disponde de una función nativa: eval() para convertir streams JSON en objetos con propiedades que son accesibles sin necesidad de manipular ninguna cadena de caracteres).

La especificación JSR 353 proporciona un API para el procesamiento de datos JSON (parsing, transformación y consultas).

La gramática de los objetos JSON es bastante simple. Sólo se requieren dos estructuras: objetos y arrays. Un objeto es un conjunto de pares nombre-valor, y un array es una lista de valores. JSON define siete tipos de valores: string, number, object, array, true, false, y null.

El siguiente ejemplo muestra datos JSON para un objeto que contiene pares nombre-valor:

Ejemplo formato JSON
{ "nombre": "John",
  "apellidos": "Smith",
  "edad": 25,
  "direccion": { "calle": "21 2nd Street",
                 "ciudad": "New York",
                 "codPostal": "10021"
               },
  "telefonos": [
         { "tipo": "fijo",
           "numero": "212 555-1234"
         },
         {
           "tipo": "movil",
           "numero": "646 555-4567"
         }
  ]
 }

El objeto anterior tiene cinco pares nombre-valor:

  • Los dos primeros son nombre y apellidos, con el valor de tipo String

  • El tercero es edad, con el valor de tipo number

  • El cuarto es direccion, con el valor de tipo object

  • El quinto es telefonos, cuyo valor es de tipo array, con dos objetos

JSON tiene la siguiente sintaxis:

  • Los objetos están rodeados por llaves {}, sus pares de elementos nombre-valor están separados por una coma ,, y el nombre y el valor de cada par están separados por dos puntos :. Los nombres en un objeto son de tipo String, mientras que sus valores pueden ser cualquiera de los siete tipos que ya hemos indicado, incluyendo a otro objeto, u otro array.

  • Los arrays están rodeados por corchetes [], y sus valores están separados por una coma ,. Cada valor en un array puede ser de un tipo diferente, incluyendo a otro objeto o array.

  • Cuando los objetos y arrays contienen otros objetos y/o arrays, los datos adquieren una estructura de árbol

Los servicios web RESTful utilizan JSON habitualmente tanto en las peticiones, como en las respuestas. La cabecera HTTP utilizada para indicar que el contenido de una petición o una respuesta es JSON es la siguiente:

Content-Type: application/json

La representación JSON es normalmente más compacta que las representaciones XML debido a que JSON no tiene etiquetas de cierre. A diferencia de XML, JSON no tiene un "esquema" de definición y validación de datos ampliamente aceptado.

Actualmente, las aplicaciones Java utilizan diferentes librerías para producir/consumir JSON, que tienen que incluirse junto con el código de la aplicación, incrementando así el tamaño del archivo desplegado. El API de Java para procesamiento JSON proporciona un API estándar para analizar y generar JSON, de forma que las aplicaciones que utilicen dicho API sean más "ligeras" y portables.

Para generar y parsear datos JSON, hay dos modelos de programación, que son similares a los usados para documentos XML:

  • El modelo de objetos: crea un árbol en memoria que representa los datos JSON

  • El modelo basado en streaming: utiliza un parser que lee los datos JSON elemento a elemento (uno cada vez).

Java EE incluye soporte para JSR 353, de forma que el API de java para procesamiento JSON se encuentra en los siguientes paquetes:

  • El paquete javax.json contiene interfaces para leer, escribir y construir datos JSON, según el modelo de objetos, así como otras utilidades.

  • El paquete javax.json.stream contiene una interfaz para parsear y generar datos JSON para el modelo streaming

Vamos a ver cómo producir y consumir datos JSON utilizando cada uno de los modelos.

5.3. Modelo de procesamiento basado en el modelo de objetos

En este caso se crea un árbol en memoria que representa los datos JSON (todos los datos). Una vez construido el árbol, se puede navegar por él, analizarlo, o modificarlo. Esta aproximación es muy flexible y permite un procesamiento que requiera acceder al contenido completo del árbol. En contrapartida, normalmente es más lento que el modelo de streaming y requiere utilizar más memoria. El modelo de objetos genera una salida JSON navegando por el árbol entero de una vez.

El siguiente código muestra cómo crear un modelo de objetos a partir de datos JSON desde un fichero de texto:

Creación de un modelos de objetos a partir de datos JSON
import java.io.FileReader;
import javax.json.Json;
import javax.json.JsonReader;
import javax.json.JsonStructure;
...
JsonReader reader = Json.createReader(new FileReader("datosjson.txt"));
JsonStructure jsonst = reader.read();

El objeto jsonst puede ser de tipo JsonObject o de tipo JsonArray, dependiendo de los contenidos del fichero. JsonObject y JsonArray son subtipos de JsonStructure. Este objeto representa la raíz del árbol y puede utilizarse para navegar por el árbol o escribirlo en un stream como datos JSON.

Vamos a mostrar algún ejemplo en el que utilicemos un StringReader.

Objeto JSON con dos pares nombre-valor
jsonReader = Json.createReader(new StringReader("{"
                      + "  \"manzana\":\"roja\","
                      + "  \"plátano\":\"amarillo\""
                      + "}"));
JsonObject json = jsonReader.readObject();
json.getString("manzana"); (1)
json.getString("plátano");
1 El método getString() devuelve el valor del string para la clave especificada como parámetro. Pueden utilizarse otros métodos getXXX() para acceder al valor correspondiente de la clave en función del tipo de dicho objeto.

Un array con dos objetos, cada uno de ellos con un par nombre-valor puede leerse como:

Array con dos objetos
jsonReader = Json.createReader(new StringReader("["
                        + "  { \"manzana\":\"rojo\" },"
                        + "  { \"plátano\":\"amarillo\" }"
                        + "]"));
JsonArray jsonArray = jsonReader.readArray(); (1)
1 La interfaz JsonArray también tiene métodos get para valores de tipo boolean, integer, y String en el índice especificado (esta interfaz hereda de java.util.List)

5.3.1. Creación de un modelos de objetos desde el código de la aplicación

A continuación mostramos un ejemplo de código para crear un modelo de objetos mediante programación:

Ejemplo de creación de un modelo de objetos JSON desde programación
import javax.json.Json;
import javax.json.JsonObject;
...
JsonObject modelo =
    Json.createObjectBuilder() (1)
        .add("nombre", "Duke")
        .add("apellidos", "Java")
        .add("edad", 18)
        .add("calle", "100 Internet Dr")
        .add("ciudad", "JavaTown")
        .add("codPostal", "12345")
        .add("telefonos",
              Json.createArrayBuilder() (2)
                  .add(Json.createObjectBuilder()
                           .add("tipo", "casa")
                           .add("numero", "111-111-1111"))
                  .add(Json.createObjectBuilder()
                           .add("tipo", "movil")
                           .add("numero", "222-222-2222")))
            .build();
1 El tipo JsonObject representa un objeto JSON. El método Json.createObjectBuilder() crea un modelo de objetos en memoria añadiendo elementos desde el código de nuestra aplicación
2 El método Json.createArrayBuilder() crea un modelo de arrays en memoria añadiendo elementos desde el código de nuestra aplicación

El objeto modelo, de tipo JsonObject representa la raíz del árbol, que es creado anidando llamadas a métodos ´add()´, y construyendo el árbol a través del método build().

La estructura JSON generada es la siguiente:

Ejemplo formato JSON generado mediante programación
{ "nombre": "Duke",
  "apellidos": "Java",
  "edad": 18,
  "calle": "100 Internet Dr",
  "ciudad": "JavaTown",
  "codPostal": "12345",
  "telefonos": [
         { "tipo": "casa",
           "numero": "111-111-1111"
         },
         {
           "tipo": "movil",
           "numero": "222-222-2222"
         }
  ]
 }

5.3.2. Navegando por el modelo de objetos

A continuación mostramos un código de ejemplo para navegar por el modelo de objetos:

import javax.json.JsonValue;
import javax.json.JsonObject;
import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonString;
...
public static void navegarPorElArbol(JsonValue arbol, String clave) {
  if (clave != null)
    System.out.print("Clave " + clave + ": ");
  switch(arbol.getValueType()) {
    case OBJECT:
       System.out.println("OBJETO");
       JsonObject objeto = (JsonObject) arbol;
       for (String nombre : object.keySet())
          navegarPorElArbol(object.get(nombre), name);
       break;
    case ARRAY:
       System.out.println("ARRAY");
       JsonArray array = (JsonArray) arbol;
       for (JsonValue val : array)
         navegarPorElArbol(val, null);
       break;
    case STRING:
       JsonString st = (JsonString) arbol;
       System.out.println("STRING " + st.getString());
       break;
    case NUMBER:
       JsonNumber num = (JsonNumber) arbol;
       System.out.println("NUMBER " + num.toString());
       break;
    case TRUE:
    case FALSE:
    case NULL:
       System.out.println(arbol.getValueType().toString());
       break;
    }
}

El método navegarPorElArbol() podemos usarlo con el ejemplo anterior de la siguiente forma:

navegarPorElArbol(modelo, "OBJECT");

El método navegarPorElArbol() tiene dos argumentos: un elemento JSON y una clave. La clave se utiliza para imprimir los pares clave-valor dentro de los objetos. Los elementos en el árbol se representan por el tipo JsonValue. Si el elemento es un objeto o un array, se realiza una nueva llamada a este método es invocada para cada elemento contenido en el objeto o el array. Si el elemento es un valor, éste se imprime en la salida estándar.

El método JsonValue.getValueType() identifica el elemento como un objeto, un array, o un valor. Para los objetos, el método JsonObject.keySet() devuelve un conjunto de Strings que contienene las claves de los objetos, y el método JsonObject.get(String nombre) devuelve el valor del elemento cuya clave es nombre. Para los arrays, JsonArray implementa la interfaz List<JsonValue>. Podemos utilizar bucles for mejorados, con el valor de Set<String> devuelto por JsonObject.keySet(), y con instancias de JsonArray, tal y como hemos mostrado en el ejemplo.

5.3.3. Escritura de un modelo de objetos en un stream

Los modelos de objetos creados en los ejemplos anteriores, pueden "escribirse" en un stream, utilizando la clase JsonWriter, de la siguiente forma:

import java.io.StringWriter;
import javax.json.JsonWriter;
...
StringWriter stWriter = new StringWriter();
JsonWriter jsonWriter = Json.createWriter(stWriter); (1)
jsonWriter.writeObject(modelo); (2)
jsonWriter.close(); (3)

String datosJson = stWriter.toString();
System.out.println(datosJson);
1 El método Json.createWriter() toma como parámetro un OutputStream
2 El método JsonWriter.writeObject() "escribe" el objeto JsonObject en el stream
3 El método JsonWriter.close() cierra el stream de salida

5.3.4. Modelo de procesamiento basado en streaming

El modelo de streaming utiliza un parser basado en eventos que va leyendo los datos JSON de uno en uno. El parser genera eventos y detiene el procesamiento cuando un objeto o array comienza o termina, cuando encuentra una clave, o encuentra un valor. Cada elemento puede ser procesado o rechazado por el código de la aplicación, y a continuación el parser continúa con el siguiente evento. Esta aproximación es adecuada para un procesamiento local, en el cual el procesamiento de un elemento no requiere información del resto de los datos. El modelo de streaming genera una salida JSON para un determinado stream realizando una llamada a una función con un elemento cada vez.

A continuación veamos con ejemplos cómo utilizar el API para el modelo de streaming:

  • Para leer datos JSON utilizando un parser (JsonParser)

  • Para escribir datos JSON utilizando un generador (JsonGenerator)

Lectura de datos JSON

El API para el modelo streaming es la aproximación más eficiente para "parsear" datos JSON utilizando eventos:

import javax.json.Json;
import javax.json.stream.JsonParser;
...
JsonParser parser = Json.createParser(new StringReader(datosJson));
while (parser.hasNext()) {
  JsonParser.Event evento = parser.next();
  switch(evento) {
    case START_ARRAY:
    case END_ARRAY:
    case START_OBJECT:
    case END_OBJECT:
    case VALUE_FALSE:
    case VALUE_NULL:
    case VALUE_TRUE:
        System.out.println(evento.toString());
        break;
    case KEY_NAME:
        System.out.print(evento.toString() + " " + parser.getString() + " - ");
        break;
    case VALUE_STRING:
    case VALUE_NUMBER:
        System.out.println(evento.toString() + " " + parser.getString());
        break;
  }
}

El ejemplo consta de tres pasos:

  1. Obtener una instancia de un parser invocando el método estático Json.createParser()

  2. Iterar sobre los eventos del parser utilizando los métodos JsonParser.hasNext() y JsonParser.next()

  3. Realizar un procesamiento local para cada elemento

El ejemplo muestra los diez posibles tipos de eventos del parser. El método JsonParser.next() "avanza" al siguiente evento. Para los tipos de eventos KEY_NAME, VALUE_STRING, y VALUE_NUMBER, podemos obtener el contenido del elemento invocando al método JsonParser.getString(). Para los eventos VALUE_NUMBER, podemos también usar los siguientes métodos:

  • JsonParser.isIntegralNumber

  • JsonParser.getInt

  • JsonParser.getLong

  • JsonParser.getBigDecimal

El parser genera los eventos START_OBJECT y END_OBJECT para un objeto JSON vacío: { }.

Para un objeto con dos pares nombre-valor:

{
  "manzaja":"roja", "plátano":"amarillo"
}

Mostramos los eventos generados:

{START_OBJECT
   "manzaja"KEY_NAME:"roja"VALUE_STRING,
   "plátano"KEY_NAME:"amarillo"VALUE_STRING
}END_OBJECT

Los eventos generados para un array con dos objetos JSON serían los siguientes:

[START_ARRAY
  {START_OBJECT "manzaja"KEY_NAME:"roja"VALUE_STRING }END_OBJECT,
  {START_OBJECT "plátano"KEY_NAME:"amarillo"VALUE_STRING }END_OBJECT
]END_ARRAY
Escritura de datos JSON

El siguiente código muestra cómo escribir datos JSON en un fichero utilizando el API para el modelo de streaming:

Ejemplo de escritura de datos JSON con el modelo de streaming
FileWriter writer = new FileWriter("test.txt");
JsonGenerator gen = Json.createGenerator(writer);
gen.writeStartObject()
   .write("nombre", "Duke")
   .write("apellidos", "Java")
   .write("edad", 18)
   .write("calle", "100 Internet Dr")
   .write("ciudad", "JavaTown")
   .write("codPostal", "12345")
   .writeStartArray("telefonos")
      .writeStartObject()
         .write("tipo", "casa")
         .write("numero", "111-111-1111")
      .writeEnd()
      .writeStartObject()
         .write("tipo", "movil")
         .write("numero", "222-222-2222")
      .writeEnd()
   .writeEnd()
   .writeEnd();
gen.close();

Este ejemplo obtiene un generador JSON invocando al método estático Json.createGenerator(), que toma como parámetro un output stream o un writer stream. El ejemplo escribe los datos JSON en el fichero text.txt anidando llamadas a los métodos write(), writeStartArray(), writeStartObject(), and writeEnd(). El método JsonGenerator.close() cierra el output stream o writer stream subyacente.

5.4. Pruebas de servicios REST

Hasta ahora hemos visto varias formas de "probar" nuestros servicios REST: desde línea de comandos con Curl, desde IntelliJ con la herramienta Test RESTFul Web Service, y desde el navegador Chrome, con Postman (siendo esta última la que más hemos utilizado).

Vamos a ver cómo implementar tests para nuestros servicios REST utilizando Maven y JUnit. Para ello repasaremos algunas cuestiones básicas sobre los ciclos de vida de Maven.

5.4.1. Ciclo de vida de Maven y tests JUnit

Un ciclo de vida en Maven es una secuencia de acciones determinada, que define el proceso de construcción de un proyecto en concreto. Como resultado del proceso de construcción de un proyecto obtendremos un artefacto (fichero), de un cierto tipo (por ejemplo .jar, .war, .ear,…​). Por lo tanto, podríamos decir que un ciclo de vida está formado por las acciones necesarias para convertir nuestros archivos fuente que constituyen el proyecto en, por ejemplo un .jar, un .war,…​

Maven propone 3 ciclos de vida, es decir, tres posibles secuencias de acciones, que podemos utilizar (y modificar a nuestra conveniencia) para construir nuestro proyecto. Dichos ciclos de vida son: clean, site y el denominado default-lifecycle.

Cada ciclo de vida está formado por fases. Una fase es un concepto abstracto, y define el tipo de acciones que se deberían llevar a cabo. Por ejemplo una fase del ciclo de vida por defecto es compile, para referirse a las acciones que nos permiten convertir los ficheros .java en los ficheros .class correspondientes.

Cada fase está formada por un conjunto de goals, que son las acciones que se llevarán a cabo en cada una de las fases. Las goals no "viven" de forma independiente, sino que cualquier goal siempre forma parte de un plugin Maven. Podríamos decir que un plugin, por lo tanto, es una agrupación lógica de una serie de goals relacionadas. Por ejemplo, el plugin wildfly, contiene una serie de goals para desplegar, re-desplegar, deshacer-el-despliegue, arrancar el servidor, etc., es decir, agrupa las acciones que podemos realizar sobre el servidor wildfly. Una goal se especifica siempre anteponiendo el nombre del plugin al que pertenece seguido de dos puntos, por ejemplo wildfly:deploy indica que se trata de la goal deploy, que pertenece al plugin wildfly de maven.

Pues bien, por defecto, Maven asocia ciertas goals a las fases de los tres ciclos de vida. Cuando se ejecuta una fase de un ciclo de vida, por ejemplo mvn package se ejecutan todas las goals asociadas a todas las fases anteriores a la fase package, en orden, y finalmente las goals asociadas a la fase package. Por supuesto, podemos alterar en cualquier momento este comportamiento por defecto, incluyendo los plugins y goals correspondientes dentro de la etiqueta <build> en nuestro fichero de configuración pom.xml.

Vamos a implementar tests JUnit. Los tests, como ya habéis visto en sesiones anteriores, en el directorio src/test. Algunas normas importantes son: que los tests pertenezcan al mismo paquete lógico al que pertencen las clases Java que estamos probando. Por ejemplo, si estamos haciendo pruebas sobre las clases del paquete org.expertojava.rest, los tests deberían pertenecer al mismo paquete, aunque físicamente el código fuente y sus pruebas estarán separados (el código fuente estará en src/main, y los tests en src/test).

Para realizar pruebas sobre nuestros servicios REST, necesitamos que el servidor Wilfly esté en marcha. También necesitamos empaquetar el código en un fichero war y desplegarlo en el servidor, todo esto ANTES de ejecutar los tests.

Las acciones para arrancar el servidor Wilfly y desplegar nuestra aplicación en él, NO forman parte de las acciones (o goals) incluidas por defecto en el ciclo de vida por defecto de Maven, cuando nuestro proyecto tiene que empaquetarse como un war (etiqueta <packaging> de nuestro pom.xml). Podéis consultar aquí la lista de goals asociadas a las fases del ciclo de vida por defecto de Maven.

Por otro lado, en el ciclo de vida por defecto, se incluye una goal para ejecutar los tests, asociada a la fase test. Dicha goal es surefire:test. El problema es que, por defecto, la fase test se ejecuta ANTES de la fase package y por lo tanto, antes de empaquetar y desplegar nuestra aplicación en Wildfly.

Por lo tanto, tendremos que "alterar" convenientemente este comportamiento por defecto para que se ejecuten las acciones de nuestro proceso de construcción que necesitemos, y en el orden en el que lo necesitemos. Como ya hemos indicado antes, esto lo haremos incluyendo dichas acciones en la etiqueta <build> de nuestro pom.xml, y configurandolas convenientemente para asegurarnos que el orden en el que se ejecutan es el que queremos.

La siguiente figura muestra parte de la secuencia de fases llevadas a cabo por Maven en su ciclo de vida por defecto. Para conseguir nuestros propósitos, simplemente añadiremos la "goals" wildfly:deploy, y la asociaremos a la fase pre-integration-test, y "cambiaremos" la fase a la que está asociada la goal surefire:test para que los tests se ejecuten DESPUÉS de haber desplegado el war en Wildfly.

Configuración del pom.xml para ejecutar los tests REST

A continuación mostramos los cambios que tenemos que realizar en el fichero de configuración pom.xml

Adición de las goals wildfly:deploy y surefire:test a las fases pre-integration-test y surefire:test respectivamente
<!-- forzamos el despliegue del war generado durante la fase pre-integration-test,
     justo después de obtener dicho .war-->
<plugin>
  <groupId>org.wildfly.plugins</groupId>
  <artifactId>wildfly-maven-plugin</artifactId>
  <version>1.0.2.Final</version>
  <configuration>
    <hostname>localhost</hostname>
    <port>9990</port>
  </configuration>

  <executions>
    <execution>
      <id>wildfly-deploy</id>
      <phase>pre-integration-test</phase>
      <goals>
        <goal>deploy</goal>
      </goals>
      </execution>
  </executions>
</plugin>

<!--ejecutaremos los test JUnit en la fase integration-test,
    inmediatamente después de la fase pre-integration-test, y antes
    de la fase verify-->
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-surefire-plugin</artifactId>
   <version>2.18</version>
   <configuration>
        <skip>true</skip>
   </configuration>
   <executions>
      <execution>
        <id>surefire-it</id>
        <phase>integration-test</phase>
        <goals>
          <goal>test</goal>
        </goals>
        <configuration>
          <skip>false</skip>
        </configuration>
      </execution>
    </executions>
</plugin>

También necesitamos incluir en el pom.xml las librerías de las que depende el código de pruebas de nuestro proyecto (clases XXXXTest situadas en src/test): librería JUnit, JAXB y el API cliente de JAX-RS. Por lo que añadimos en el las dependencias correspondientes:

Dependencias del código src/test con JUnit y API cliente de JAX-RS
...
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.jboss.resteasy</groupId>
  <artifactId>resteasy-client</artifactId>
  <version>3.0.13.Final</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.jboss.resteasy</groupId>
  <artifactId>resteasy-jaxb-provider</artifactId>
  <version>3.0.13.Final</version>
</dependency>
...

Dado que vamos a trabajar con el API Json, y dado que ejecutaremos los tests desde la máquina virtual de Java, y no dentro del servidor WildFly, necesitamos añadir también las siguientes librerías:

Dependencias del código src/test con el API Json de jaxrs
...
<!--Librerías para serializar/deserializar json -->
<dependency>
  <groupId>org.jboss.resteasy</groupId>
  <artifactId>resteasy-jackson-provider</artifactId>
  <version>3.0.13.Final</version>
</dependency>

<!--Jaxrs API json -->
<dependency>
  <groupId>org.jboss.resteasy</groupId>
  <artifactId>resteasy-json-p-provider</artifactId>
  <version>3.0.13.Final</version>
</dependency>
...

No hemos incluido en el pom.xml la orden para arrancar Wildfly. Vamos a hacer esto desde IntelliJ en un perfil de ejecución, como ya habéis hecho en sesiones anteriores. De esta forma, podremos ver desde IntelliJ la consola de logs del servidor. En este caso, podemos crear un perfil solamente para arrancar el servidor Wildfly (no es necesario que se incluya el despliegue del war generado, puesto que lo haremos desde la ventana Maven Projects). Antes de iniciar el proceso de construcción, por lo tanto, tendremos que asegurarnos de que hemos arrancado Wildlfly.

Con estos cambios en el pom.xml, y ejecutando el comando mvn verify se llevarán a cabo las siguientes acciones, en este orden:

  • Después de compilar el proyecto, obtenemos el .war (fase package)

  • El .war generado se despliega en el servidor de aplicaciones Wilfly (fase pre-integration-test)

  • Se ejecutan los test JUnit sobre la aplicación desplegada en el servidor (fase integration-test)

5.4.2. Anotaciones JUnit y aserciones AssertThat

JUnit 4 proporciona anotaciones para forzar a que los métodos anotados con @Test se ejecuten en el orden que nos interese (por defecto no está garantizado que se ejecuten en el orden en el que se escriben).

En principio, debemos programar los tests para que sean totalmente independientes unos de otros, y por lo tanto, el orden de ejecución no influya para nada en el resultado de su ejecución, tanto si se ejecuta el primero, como a mitad, o el último. El no hacer los tests independientes hace que el proceso de testing "se alargue" y complique innecesariamente, ya que puede ser que unos tests "enmascaren" en resultado de otros, o que no podamos "saber" si ciertas partes del código están bien o mal implementadas hasta que los tests de los que dependemos se hayan superado con éxito.

Aún así, y dado que muchas veces se obtienen errores por hacer asunciones en el orden de la ejecución de los tests, JUnit nos permite fijar dicho orden. Para ello utilizaremos la anotación @FixMethodOrder, indicando el tipo de ordenación, como por ejemplo MethodSorters.NAME_ASCENDING, de forma que se ejecutarán los tests por orden lexicográfico.

Por ejemplo:

Ejemplo para forzar el orden de ejecución de los test (orden lexicográfico)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestMethodOrder {

    @Test
    public void testB() {
       System.out.println("second");
     }

    @Test
    public void testA() {
        System.out.println("first");
    }

    @Test
    public void testC() {
        System.out.println("third");
    }
}

En ese caso, el orden de ejecución será: testA(), a continuación testB(), y finalmente testC().

Otra aportación de JUnit 4 es la incorporación de aserciones de tipo assertThat. En sesiones anteriores habéis utilizado aserciones con métodos Assert.assertEquals(resultado_esperado, resultado_real). Los nuevos métodos Assert.assertThat() permiten una mayor flexibilidad a la hora de expresar las aserciones realizadas en nuestros tests, así como una mayor legibilidad de los mismos. El prototipo general de las aserciones de este tipo es:

assertThat([value], [matcher statement]);

en donde [value] es el resultado real (valor sobre el que se quiere afirmar algo), y [matcher statement] es un Matcher u objeto que realiza operaciones de "emparejamiento" sobre una secuencia de caracteres según un determinado patrón.

Por ejemplo:

Ejemplos de sentencias assertThat
assertThat(x, is(not(4)));  (1)
assertThat(responseStringJson, either(containsString("nombre")).and(containsString("apellido"))); (2)
assertThat(myList, hasItem("3")); (3)
1 Aquí utilizamos un matcher con el patrón "4", esta sentencia devuelve false si x != "4"
2 Podemos "combinar" varios matchers, de forma que se tengan que satisfacer más de uno
3 En este caso aplicamos el matcher sobre un conjunto de elementos

Hay varias librerías que implementan Matchers. JUnit incluye parte de los matchers de Hamcrest (Hamcrest es un framework para escribir objetos matcher permitiendo definir reglas de matching de forma declarativa). Otra librería interesante para realizar testing de servicios rest que utilizan representaciones Json es la librería: hamcrest-json, que podemos utilizar para realizar aserciones sobre dos objetos Json.

Por ejemplo, supongamos que nuestro objeto Json contiene una lista de enlaces Hateoas, de tipo Link. Los objetos Link serán serializados/deserializados (Wildfly utiliza Jackson para realizar estas tareas) convenientemente. Cuando serializamos un objeto Link (obtenemos su representación Json), veremos, además de los objetos "uri": "valor", "type": "valor" y "rel": "valor", que son los que básicamente utilizamos al crear los enlaces Hateoas, otros como "uriBuilder": {…​}, "params":{…​}, que puede que no nos interese consultar, o incluso que no les hayamos asignado ningún valor.

Si en nuestro test, queremos comprobar que el objeto Json que nos devuelve el servicio (resultado real) se corresponde con el valor esperado, tendremos que "comparar" ambas representaciones. Ahora bien, puede que solamente nos interese comparar ciertos valores contenidos en el objeto Json, no el objeto "completo".

Hacer esta comprobación "elemento a elemento" es bastante tedioso. La librería hamcrest-json nos proporciona lo que estamos buscando, con los métodos sameJSONAs(),allowingExtraUnexpectedFields(), y allowingAnyArrayOrdering(), de la siguiente forma:

Método para comparar dos representaciones Json. Clase uk.co.datumedge.hamcrest.json.SameJSONAs
Assert.assertThat("{\"age\":43, \"friend_ids\":[16, 52, 23]}",
                  sameJSONAs("{\"friend_ids\":[52, 23, 16]}")
                            .allowingExtraUnexpectedFields()
                            .allowingAnyArrayOrdering());

En este código tenemos una representación formada por dos objetos, uno de los cuales tiene como valor un array de enteros. Si el servicio rest devuelve un objeto Json con más elementos, o en otro orden, en este caso el resultado de la sentencia assertThat es true. Volviendo al ejemplo anterior de un objeto Json que contiene enlaces Hatehoas, podríamos realizar la siguiente comparación:

Comparamos dos objetos Json que contienen hiperenlaces Hateoas (objetos Link)
JsonObject json_object =
    client.target("http://localhost:8080/foro/usuarios")
          .request(MediaType.APPLICATION_JSON)
          .get(JsonObject.class);   (1)

String json_string = json_object.toString();  (2)

JsonObject usuarios =
   Json.createObjectBuilder()
       .add("usuarios",
           Json.createArrayBuilder()
              .add(Json.createObjectBuilder()
                  .add("nombre", "Pepe Lopez")
                  .add("links",
                      Json.createArrayBuilder()
                        .add(Json.createObjectBuilder()
                        .add("uri", "http://localhost:8080/foro/usuarios/pepe")
                        .add("type", "application/xml,application/json")
                        .add("rel", "self"))))
              .add(Json.createObjectBuilder()
                  .add("nombre", "Ana Garcia")
                  .add("links",
                      Json.createArrayBuilder()
                        .add(Json.createObjectBuilder()
                        .add("uri", "http://localhost:8080/foro/usuarios/ana")
                        .add("type", "application/xml,application/json")
                        .add("rel", "self")))))
    .build();   (3)

Assert.assertThat(json_string,
                  sameJSONAs(usuarios.toString())
                      .allowingExtraUnexpectedFields()
                      .allowingAnyArrayOrdering()); (4)
1 Realizamos la llamada al servicio REST y recibimos como respuesta un objeto Json. En este caso nuestro objeto Json está formado por una lista de objetos.
2 Obtenemos la representación de nuestro objeto Json (resultado real) en forma de cadena de caracteres
3 Creamos un nuevo objeto Json con el resultado esperado
4 Comparamos ambos objetos. Si el resultado real incluye más elementos que los contenidos en json_string o en otro orden consideraremos que hemos obtenido la respuesta correcta.

Para utilizar esta librería en nuestro proyecto, simplemente tendremos que añadirla como dependencia en la configuración de nuestro pom.xml:

Librería para comparar objetos Json en los tests
<!--Hamcrest Json -->
<dependency>
    <groupId>uk.co.datumedge</groupId>
    <artifactId>hamcrest-json</artifactId>
    <version>0.2</version>
</dependency>

5.4.3. Observaciones sobre los tests y algunos ejemplos de tests

Recuerda que para utilizar el API cliente, necesitas utilizar instancias javax.ws.rs.client.Client, que debemos "cerrar" siempre después de su uso para cerrar el socket asociado a la conexión.

Para ello podemos optar por: * Crear una única instancia Client "antes" de ejecutar cualquier test (método @BeforeClass), y cerrar el socket después de ejecutar todos los tests (método @AfterClass) * Crear una única instancia Client "antes" de ejecutar CADA test (método @Before), y cerrar el socket después de ejecutar CADA tests (método @After)

Si el resultado de una invocación sobre la instancia Client es de tipo javax.ws.rs.core.Response, debemos liberar de forma explícita la conexión para que pueda ser usada de nuevo por dicha instancia Client.

Por ejemplo, supongamos que queremos realizar un test en el que realizamos una operación POST, y a continuación una operación GET para verificar que el nuevo recurso se ha añadidido correctamente:

Ejemplo de Test que utiliza una instancia Client para todos los tests
public class TestRESTServices {
  private static final String BASE_URL = "http://localhost:8080/rest/";
  private static URI uri = UriBuilder.fromUri(BASE_URL).build();
  private static Client client;

  @BeforeClass
  public static void initClient() {
    client = ClientBuilder.newClient(); (1)
  }

  @AfterClass
  public static void closeClient() {
    client.close(); (2)
  }

  @Test
  public void createAndRetrieveACustomer() {

    Customer customer = ... //Creamos un nuevo cliente
    Response response = client.target(uri)
                          .request()
                          .post(Entity.entity(customer, MediaType.APPLICATION_JSON));
    assertEquals(Response.Status.CREATED, response.getStatusInfo());
    URI referenceURI = response.getLocation();
    response.close(); (3)

    // Obtenemos el recurso que hemos añadido
    response = client.target(referenceURI).request().get();

    Customer retrieved_customer = response.readEntity(Customer.class);
    assertEquals(Response.Status.OK, response.getStatusInfo());
    assertEquals(retreivedRef.getName(), r.getName());
    response.close(); (4)
  }
}
1 Creamos una instancia Client ANTES de ejecutar cualquier test
2 Cerramos el socket asociado a la conexión DESPUÉS de ejecutar TODOS los tests
3 Liberamos la conexión para poder reutilizarla
4 Liberamos la conexión para poder reutilizarla

Ahora veamos otro ejemplo en el que utilizamos una instancia Client para cada test:

Ejemplo de Test que utiliza una instancia Client para CADA test
public class TestRESTServices {

  private Client client;

  @Before
  public void setUp() {
      this.client = ClientBuilder.newClient(); (1)
  }

  @After
  public void tearDown() {
      this.client.close();  (2)
  }

  @Test
  public void getAllCustomersAsJson() {
     String uriString = "http://localhost:8080/rest/customers";
     JsonArray json_array = client
                .target(uriString)
                .request(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .get(JsonArray.class);

     Assert.assertEquals(2, json_array.size());
   }

   @Test
   public void getAllCustomers() {
    String uriString = "http://localhost:8080/rest/customers";
    //Consultamos los datos de todos los customers
    List<Customer> lista_usuarios = client.target(uriString)
                .request("application/json")
                .get(new GenericType<List<Customer>>() {});
    Assert.assertEquals(2, lista_usuarios.size());
  }
}
1 Creamos una instancia Client ANTES de ejecutar CADA test
2 Cerramos el socket asociado a la conexión DESPUÉS de ejecutar CADA los tests

Podemos ver en este último ejemplo que no es necesario liberar la conexión entre usos sucesivos de la instancia Client, si no utilizamos la clase Response. En este caso el proceso se realiza de forma automática por el sistema.

Finalmente comentaremos que, debido a un bug en la especificación JAX-RS, el deserializado del de los objetos Link no se realiza, por lo que obendremos una lista de Links vacía (ver http://kingsfleet.blogspot.com.es/2014/05/reading-and-writing-jax-rs-link-objects.html). Podemos comprobar que, si obtenemos la representación en formato texto de la entidad del mensaje, dicha lista de objetos tendrá el valor correcto.

Si no utilizamos la solución propuesta en el enlace anterior, deberemos usar la anotación @JsonIgnoreProperties(ignoreUnknown = true). De esta forma, ignoraremos el deserializado de los objetos Link, pero tendremos que utilizar la representación en formato de cadena de caracteres del recurso json, en lugar del objeto java Link asociado.

Así, por ejemplo, si nuestro recurso Customer tiene asociado una lista de objetos Link, para poder utilizar el API Cliente y acceder a la lista de enlaces, usaremos la anotación anterior en la implementación de la clase Customer:

@JsonIgnoreProperties(ignoreUnknown = true)
@XmlRootElement(name="customer")
public class Customer {
  int id;
  String name;
  ...
  List<Link> links;
  ...
}

5.5. Ejercicios

5.5.1. Tests utilizando el API cliente y un mapeador de excepciones (1 punto)

Se proporciona como plantilla el MÓDULO IntelliJ "s5-tienda" con una implementación parcial de una tienda de clientes on-line. Este proyecto ya contiene varios tests implementados, a modo de ejemplo.

Los recursos rest implementados lanzan excepciones de tipo RestException si, por ejemplo se intenta realizar una consulta sobre un producto y/o usuario que no existe.

Se ha implementado un mapeador de excepciones RestExceptionMapper que captura excepciones de tipo RuntimeException, y devuelve una respuesta de tipo ErrorMensajeBean que será serializada a formato json y/o formato xml (dependiendo del valor de la cabecera Accept de la petición), con información sobre el error producido.

Implementa los siguientes dos tests: * test7recuperarTodosLosUsuarios(), en el que realizamos una invocación GET sobre "http://localhost:8080/s5-tienda/rest/clientes/". Esta URI podría corresponderse con un método anotado con @GET y que devolviese una lista de todos los clientes de la tienda. Sin embargo, no existe tal método en nuestro recursos rest. Verifica que dicha invocación devuelve el código de estado "500" (Internal Server Error), y que en el cuerpo del mensaje se recibe "Servicio no disponible"

  • test8recuperarClienteQueNoExiste(), en el que intentamos recuperar la información de un cliente que no exista en nuestra base de datos. En este caso, debemos verificar que el mensaje obtenido en formato json es el siguiente:

    {
      "status": "Not Found",
      "code": 404,
      "message": "El producto no se encuentra en la base de datos",
      "developerMessage": "error"
    }

5.5.2. Tests utilizando el API Json y JUnit (1 punto)

Vamos a seguir usando el proyecto s4-foroAvanzado con el que hemos trabajado en la sesión anterior.

Vamos a implementar algunos tests con JUnit en los que utilizaremos, además del API cliente, el API Json, que nos proporciona jaxrs.

Para ejecutar los tests necesitamos modificar el pom.xml añadiendo las dependencias correspondientes que hemos visto a lo largo de la sesión, y añadiendo las goals para que se ejecuten los tests después de desplegar la aplicación en Wildfly.

Proporcionamos el contenido del pom.xml con las librerías y plugins que necesitarás (aunque como ejercicio deberías intentar modificar la configuración tú mismo, y luego puedes comprobar el resultado con el pom.xml que se proporciona). El contenido del nuevo pom.xml lo tienes en /src/test/resources/nuevo-pom.mxl.

Inicialización de los datos para los tests

Vamos a utilizar DBUnit para inicializar la BD para realizar los tests. Para ello tendrás que añadir en el pom.xml las dependencias necesarias (ya están añadidas en el fichero de configuración proporcionado). En el fichero src/test/resources/foro-inicial.xml encontraréis el conjunto de datos con el que inicializaremos la base de datos para ejecutar nuestros tests.

No es necesario (aunque es una muy buena práctica) que inicialicemos la BD para cada test.

5.5.3. Implementación de los tests

Vamos a implementar los siguientes tests (que se ejecutarán en en este mismo orden):

  • test1ConsultaTodosUsuarios(): recuperamos los datos de todos los usuarios del foro. Recuerda que previamente tienes que haber inicializado la BD con los datos del fichero foro-inicial.xml. Recupera los datos en forma de JsonObject y comprueba que el número de usuarios es el correcto. También debes comprobar que tanto el login, como los enlaces hatehoas para cada usuario están bien creados. En concreto, para cada usuario, debes verificar que la uri ("uri"), el tipo mime ("type"), y el tipo de enlace ("rel") son los correctos.

  • test2CreamosMensajeDePepe(): crearemos un nuevo mensaje del usuario con login "pepe". Recuerda que este usuario tiene el rol "registrado". El mensaje tendrá el asunto "cena", y el texto será: "Mejor me voy al cine". En este caso, deberás comprobar el valor de estado (debe ser 201), y debes recuperar (consultar con una petición REST) el mensaje para comprobar que la operación de insertar el mensaje ha tenido éxito.

  • test3CreamosMensajeDeUsuarioNoAutorizado(): creamos un nuevo mensaje de un usuario que no está autorizado (por ejemplo, de un usuario con login "juan"). En este caso el mensaje tendrá el asunto "cena", y el mensaje puede ser: "Pues yo tampoco voy". El resultado debe ser el código de estado 401 ( Unauthorized)

  • test4ConsultaUsuario(): Consultamos los datos del usuario "pepe". Recuperaremos los datos como un JsonObject, y comprobaremos que el valor de la "uri" para el tipo de relación "self" del enlace Link asociado es el correcto