Mostrando entradas con la etiqueta REST Assured. Mostrar todas las entradas
Mostrando entradas con la etiqueta REST Assured. Mostrar todas las entradas

domingo, 7 de enero de 2024

Wiremock y Quarkus: Como configurarlo

Wiremock es una gran herramienta que puede ayudarte en la realización de pruebas de calidad de tu código. Un paso fundamental a la hora de desarrollar pruebas de aplicaciones o servicios que invocan a otros. Las pruebas son un paso fundamental en el desarrollo de software. Y Quarkus es un framework Java diseñado especialmente para el desarrollo de aplicaciones en contenedores, sin servidor y en la nube, especialmente para Kubernetes. De ambos hemos hablado varias veces ya.

Hoy veremos cómo podemos usarlos juntos, puesto que su uso no es tan sencillo, como pudiera ser con otra aplicación realizada con otro framework Java. Y para lograrlo haremos uso de la anotación @QuarkusTestResource. En una clase de testing debemos añadir dicha anotación asociada a una clase que implemente a su vez la interfaz QuarkusTestResourceLifecycleManager. Esto nos permitirá inicializar un servicio externo antes de la ejecución de las propias pruebas. En nuestro caso, nos permitirá arrancar el servidor Wiremock.

public class WiremockTestResource implements QuarkusTestResourceLifecycleManager {
  public static WireMockServer wireMockServer;
  @Override
  public Map<String, String> start() {
    wireMockServer = new WireMockServer(57001);
    wireMockServer.start();
    return new HashMap<String, String>();
  }
  @Override
  public void stop() {
    if (wireMockServer != null) {
      wireMockServer.stop();
    }
  }
}

Una vez creada la clase que permite arrancar Wiremock, haremos uso de ella a través de la anotación @QuarkusTestResource. De esta forma ya solo nos quedará indicar los stubs asociados a nuestros tests.

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class) //Resource that call external service
@QuarkusTestResource(WiremockTestResource.class)
public class WiremockResourceBasicTest {
  @Test
  public void getById() {
    Log.info("WiremockResourceBasicTest - getById");
    ObjectMapper mapper = new ObjectMapper();
    Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
    Log.info("WM port: " + WiremockTestResource.wireMockServer.getOptions().portNumber());
    WiremockTestResource.wireMockServer.stubFor(
      WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse()
      .withJsonBody(mapper.valueToTree(movie))
      .withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));
    when().get("/1").andReturn().then().statusCode(200)
      .body("id", is(1)).body("name", is("Dune")).body("director",
      is("Denis Villeneuve"));
  }
}

Como veis es muy sencillo. Ahora veremos una opción un poco más complejas pero que permitirá crear el servidor de Wiremock con más opciones de configuración y modificarlas dinámicamente. Aunque el desarrollo es un poco lioso, debido a que tiene referencias cruzadas. 

Por un lado crearemos una anotación que tenga las propiedades que queramos modificar dinámicamente posteriormente a través de la propia anotación. Pero también le añadiremos la anotación @QuarkusTestResource que hará referencia a la clase que controla el servidor de Wiremock. 

@QuarkusTestResource(value = WiremockResourceConfigurable.class, restrictToAnnotatedClass = true)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WiremockTestAnnotation {
    String port() default "57005";
}

Ahora mostraremos la clase que configura el servidor de Wiremock. Será similar al ejemplo anterior, pero con tres añadidos. Para empezar, la interfaz que vamos a implementar será QuarkusTestResourceConfigurableLifecycleManager, y está estará asociada a la anotación creada anteriormente. Luego, a través de los atributos de la anotación, en el método init, podremos configurar dinámicamente el servidor. Y por último, con el retorno del método start, también podremos sobre escribir valores del fichero application.properties con los valores dinámicos. En este caso, a través de esta sobre escritura, podremos modificar el puerto del servicio externo pre configurado para el cliente.

public class WiremockResourceConfigurable 
  implements QuarkusTestResourceConfigurableLifecycleManager<WiremockTestAnnotation> {	
    public static WireMockServer server;
    private String port;
    @Override
    public void init(WiremockTestAnnotation params) {
        port = params.port();
    }
    @Override
    public Map<String, String> start() {
	server = new WireMockServer(Integer.valueOf(port));
        server.start();
	return  Map.of(
          "quarkus.rest-client.\"com.home.example.service.ExternalService\".url", 
          "http://localhost:"+port
          );
    }
    //...
}

Ya solo nos quedaría mostrar el test, haciendo uso de la anotación que nos permitiría modificar el puerto del servidor wiremock y del servicio externo dinámicamente. Muy similar al anterior. 

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
@WiremockTestAnnotation(port = "57005")
public class WiremockResourceTest {
    @Inject
    @ConfigProperty(name = "quarkus.rest-client.\"com.home.example.service.ExternalService\".url")
    private String serverUrl;
    @Test
    public void getById() {
        Log.info("serverUrl: " + serverUrl);
        ObjectMapper mapper = new ObjectMapper();
        Movie movie = new Movie(1L, "Denis Villeneuve", "Dune");
        WiremockResourceConfigurable.server.stubFor(
            WireMock.get("/imdb/film/1").willReturn(WireMock.aResponse().withJsonBody(mapper.valueToTree(movie))
	    .withStatus(200).withHeader(HttpHeaders.CONTENT_TYPE, "application/json")));	
        when().get("/1").andReturn().then().statusCode(200)
            .body("id", is(1)).body("name", is("Dune")).body("director",is("Denis Villeneuve"));
    }
}

Con esto hemos visto una particularidad más para mejorar los test en Quarkus. Y aunque Wiremock es una gran herramienta que desde hace tiempo es muy util para el desarrollo de pruebas. No quisiera omitir, que también es posible mockear el servicio externo de una forma sencilla con Quarkus.  Y esto es posible haciendo uso de las anotaciones @InjectMock y @RestClient. En el ejemplo que mostramos a continuación, ExternalService ha sido todo el tiempo el cliente web que hemos mockeado con Wiremock.  

@QuarkusTest
@TestHTTPEndpoint(WiremockResource.class)
public class WiremockResourceInjectTest {
    @InjectMock
    @RestClient
    ExternalService extService;
    @Test
    public void getById() {
        Mockito.when(extService.getMovieById(Mockito.anyLong())).thenReturn(new Movie(1L, "Denis Villeneuve", "Dune"));
        when().get("/1").andReturn().then().statusCode(200).body("id", is(1)).body("name", is("Dune")).body("director",
            is("Denis Villeneuve"));
    }
}

Espero que como siempre haya sido util, y si quieres puedes ver el código aquí

sábado, 4 de mayo de 2019

REST Assured y como probar tus APIs REST


Hoy vamos a ver un software que nos permita hacer las pruebas a nuestra API REST, y este es: REST Assured. Para ello utilizaremos otra vez el servicio web  que ya hicimos con Jersey.

Para empezar decir que REST Assured esta basado en dos patrones de diseños: Fluent y Behaviour-driven development. Es decir:
  • Podemos concatenar la ejecución de los métodos. 
  • Nos proporciona métodos que nos aportan mayor legibilidad (aunque a efectos practicos no tengan una mayor trascendencia).
Rest Assured tiene una múltitud de métodos que nos ayudarán facilmente y junto con Hamcrest a testear completamente una API REST. Vamos a ir viendo su funcionamiento poco a poco y a través de ejemplos, pero centrandonos sobre todo en su funcionalidad JSON y dejando de lado su funcionalidad XML que también la tiene. 
  • Testear el cuerpo de la respuesta a través de body o content. Para navegar sobre el objeto JSON nos podemos ayudar de JSONPath. En este ejemplo también podemos ver los métodos BDD: given, when o then.
@Test
public void validateBody() {
 // http://localhost:8080/JerseyExample/rest/book
 given().when().get("/JerseyExample/rest/book").then().content("id[0]", equalTo(1));
 given().when().get("/JerseyExample/rest/book").then().body("id", hasItems(1, 2, 3));
 given().when().get("/JerseyExample/rest/book/list").then().body("library.findAll { it.id < 2 }.author", hasItems("Isaac Asimov"));
}
  • Testear una sección concreta del objeto JSON, y marcarla como el centro del objeto para ayudarnos en los JSONPATH a través del método root. También vemos el uso del método BDD,  and que nos ayuda a la hora de concatenar asserts. 
get("/JerseyExample/rest/book").then().root("[0]").body("author", equalTo("Isaac Asimov")).and().body("id", equalTo(1));
  • Indicar parámetros de forma dinámica en la llamada. 
get("/JerseyExample/rest/book/{id}", 1).then().body("id", equalTo(1));
  • Validar la respuesta HTTP: cabecera, status code, cookies o content type.
get("/JerseyExample/rest/book").then().contentType("application/json");
// get("/JerseyExample/rest/book").then().cookie("JSESSIONID");
get("/JerseyExample/rest/book").then().header("Content-Type", "application/json");
get("/JerseyExample/rest/book").then().statusCode(200);
  • Validar el esquema JSON de la respuesta.
get("/JerseyExample/rest/book/1").then().assertThat().body(matchesJsonSchemaInClasspath("book-schema.json"));
  • Extraer el contenido como un conjunto de objetos y realizar asercciones posteriormente
List<Map<String, Object>> books = get("/JerseyExample/rest/book").as(new TypeRef<List<Map<String, Object>>>() {});
assertThat(books, hasSize(3));
assertThat(books.get(0).get("author"), equalTo("Isaac Asimov"));
  • Extraer el contenido o parte del contenido en un formato concreto y otros valores de la respuesta.
String json = get("/JerseyExample/rest/book/1").asString();
assertThat(json, equalTo("{\"id\":1,\"author\":\"Isaac Asimov\",\"name\":\"Foundation\"}"));
Map<String, String> allCookies = get("/JerseyExample/rest/book/1").getCookies();
assertThat(allCookies.size(), equalTo(0));
String pathValue = get("/JerseyExample/rest/book/1").path("author");
assertThat(pathValue, equalTo("Isaac Asimov"));
  • Incluir logs, de todo o solo de partes concretas. O validar el tiempo que tarda en realizarse las llamadas.
given().log().all().get("/JerseyExample/rest/book/{id}", 1).then().time(lessThan(2000L));

Como veis, se pueden hacer muchas cosas. En estos ejemplos nos hemos centrado más en la parte de validar el JSON. Y hemos dejado de lado las validaciones XML o validaciones más complejas de tipo autenticación o cross-site, que también las tiene.