RESTO asegurado: lo que aprendimos de cinco años de usar la herramienta

REST Assured : DSL para probar los servicios REST, que está integrado en las pruebas de Java. Esta solución apareció hace más de nueve años y se ha hecho popular debido a su simplicidad y funcionalidad conveniente.


En DINS, escribimos más de 17,000 pruebas con él y durante los cinco años de uso, encontramos muchas "trampas" que no se pueden encontrar inmediatamente después de importar la biblioteca al proyecto: un contexto estático, confusión en el orden en que se aplican los filtros a la consulta, dificultades para estructurar la prueba.


Este artículo trata sobre las características implícitas de REST Assured. Deben tenerse en cuenta si existe la posibilidad de que el número de pruebas en el proyecto aumente rápidamente, para que no tenga que volver a escribirlas más tarde.


imagen


¿Qué estamos probando?


DINS participa en el desarrollo de la plataforma UCaaS. En particular, desarrollamos y probamos la API que RingCentral usa y proporciona a desarrolladores externos.


Al desarrollar cualquier API, es importante asegurarse de que funcione correctamente, pero cuando lo entrega, debe verificar muchos más casos. Por lo tanto, se agregan docenas y cientos de pruebas a cada nuevo punto final. Las pruebas se escriben en Java, TestNG se selecciona como marco de prueba y REST Assured se usa para solicitudes de API.


Cuando REST Assured se beneficiará


Si su objetivo no es probar a fondo toda la API, la forma más fácil de hacerlo es REST Assured. Es muy adecuado para verificar la estructura de respuesta, PVD y pruebas de humo.


Así es como se ve una prueba simple, que verificará que el punto final proporcione el estado de 200 OK al acceder a él:


given() .baseUri("http://cookiemonster.com") .when() .get("/cookies") .then() .assertThat() .statusCode(200); 

Las palabras clave given , when y then forman la solicitud: given determina qué se enviará en la solicitud, when , con qué método y a qué punto final enviamos la solicitud, y then , cómo se verifica la respuesta recibida. Además, puede extraer el cuerpo de la respuesta en forma de un objeto de tipo JsonPath o XmlPath , para luego utilizar los datos recibidos.


Las pruebas reales suelen ser más grandes y más complicadas. Encabezados, cookies, autorización, cuerpo de solicitud se agregan a las solicitudes. Y si la API bajo prueba no consta de docenas de recursos únicos, cada uno de los cuales requiere parámetros especiales, querrá almacenar plantillas preparadas en algún lugar para agregarlas más tarde a una llamada específica en la prueba.


Para esto, en REST Assured hay:


  • RequestSpecification / ResponseSpecification ;
  • configuración básica
  • filtros

RequestSpecification y ResponseSpecification


Estas dos clases le permiten determinar los parámetros de solicitud y las expectativas de la respuesta:


 RequestSpecification requestSpec = given() .baseUri("http://cookiemonster.com") .header("Language", "en"); requestSpec.when() .get("/cookiesformonster") .then() .statusCode(200); requestSpec.when() .get("/soup") .then() .statusCode(400); 

 ResponseSpecification responseSpec = expect() .statusCode(200); given() .expect() .spec(responseSpec) .when() .get("/hello"); given() .expect() .spec(responseSpec) .when() .get("/goodbye"); 

Se utiliza una especificación en varias llamadas, pruebas y clases de prueba, según dónde se defina; no hay restricción. Incluso puede agregar múltiples especificaciones a una sola solicitud. Sin embargo, esta es una fuente potencial de problemas :


 RequestSpecification requestSpec = given() .baseUri("http://cookiemonster.com") .header("Language", "en"); RequestSpecification yetAnotherRequestSpec = given() .header("Language", "fr"); given() .spec(requestSpec) .spec(yetAnotherRequestSpec) .when() .get("/cookies") .then() .statusCode(200); 

Registro de llamadas:


 Request method: GET Request URI: http://localhost:8080/ Headers: Language=en Language=fr Accept=*/* Cookies: <none> Multiparts: <none> Body: <none> java.net.ConnectException: Connection refused (Connection refused) 

Resultó que todos los encabezados se agregaron a la llamada, pero el URI de repente se convirtió en localhost, aunque se agregó en la primera especificación.


Esto sucedió debido al hecho de que REST Assured maneja las anulaciones de los parámetros de solicitud de manera diferente (lo mismo ocurre con la respuesta). Los encabezados o filtros se agregan a la lista y luego se aplican a su vez. Solo puede haber un URI, por lo que se aplica el último. No se especificó en la última especificación agregada, por lo tanto, REST Assured lo anula con el valor predeterminado (localhost).


Si agrega una especificación a la solicitud, agregue una . El consejo parece obvio, pero cuando el proyecto con pruebas crece, aparecen clases auxiliares y clases de prueba básicas, antes de que aparezcan métodos dentro de ellas. Hacer un seguimiento de lo que realmente está sucediendo con su solicitud se vuelve difícil, especialmente si varias personas escriben pruebas a la vez.


Configuración básica REST asegurada


Otra forma de realizar consultas de plantillas en REST Assured es configurar la configuración básica y definir los campos estáticos de la clase RestAssured:


 @BeforeMethod public void configureRestAssured(...) { RestAssured.baseURI = "http://cookiemonster.com"; RestAssured.requestSpecification = given() .header("Language", "en"); RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); ... } 

Los valores se agregarán automáticamente a la solicitud cada vez. La configuración se combina con las anotaciones @BeforeMethod en TestNG y @BeforeEach en JUnit, por lo que puede estar seguro de que cada prueba que ejecute comenzará con los mismos parámetros.


Sin embargo, la configuración será una fuente potencial de problemas, porque es estática .


Ejemplo: antes de cada prueba, tomamos un usuario de prueba, obtenemos un token de autorización para él y luego lo agregamos a través de AuthenticationScheme o un filtro de autorización a la configuración básica. Mientras las pruebas se ejecuten en un solo hilo, todo funcionará.
Cuando hay demasiadas pruebas, la decisión habitual de dividir su ejecución en varios subprocesos llevará a reescribir un fragmento de código para que un token de un subproceso no caiga en el vecino.


REST Filtros asegurados


Los filtros modifican ambas solicitudes antes de enviarlas y las respuestas antes de verificar el cumplimiento de las expectativas especificadas. Ejemplo de aplicación: agregar registro o autorización:


 public class OAuth2Filter implements AuthFilter { String accessToken; OAuth2Filter(String accessToken) { this.accessToken = accessToken; } @Override public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) { requestSpec.replaceHeader("Authorization", "Bearer " + accessToken); return ctx.next(requestSpec, responseSpec); } } 

 String accessToken = getAccessToken(username, password); OAuth2Filter auth = new OAuth2Filter(accessToken); given() .filter(auth) .filter(new RequestLoggingFilter()) .filter(new ResponseLoggingFilter()) ... 

Los filtros que se agregan a la solicitud se almacenan en LinkedList . Antes de realizar una solicitud, REST Assured la modifica revisando la lista y aplicando un filtro tras otro. Luego se hace lo mismo con la respuesta que vino.


El orden de los filtros es importante . Estas dos consultas conducirán a registros diferentes: la primera indicará el encabezado de autorización, la segunda, no. En este caso, el encabezado se agregará a ambas solicitudes; solo en el primer caso, REST Assured primero agregará autorización antes de registrarse y, en el segundo, viceversa.


 given() .filter(auth) .filter(new RequestLoggingFilter()) … given() .filter(new RequestLoggingFilter()) .filter(auth) 

Además de la regla habitual de que los filtros se aplican en el orden en que se agregan, aún existe la oportunidad de priorizar su filtro mediante la implementación de la interfaz OrderedFilter . Le permite establecer una prioridad numérica especial para el filtro, por encima o por debajo del valor predeterminado (1000). Los filtros con una prioridad anterior se ejecutarán antes de lo habitual, con una prioridad inferior, después de ellos.


Por supuesto, aquí puede confundirse y establecer accidentalmente los dos filtros con la misma prioridad, por ejemplo, en 999. Luego, el que se agregó antes se aplicará primero a la solicitud.


No solo filtros


Cómo hacer la autorización a través de filtros se muestra arriba. Pero además de este método en REST Assured, hay otro, a través de AuthenticationScheme :


 String accessToken = getAccessToken(username, password); OAuth2Scheme scheme = new OAuth2Scheme(); scheme.setAccessToken(accessToken); RestAssured.authentication = scheme; 

Este es un método obsoleto. En su lugar, debe elegir el que se muestra arriba. Hay dos razones:


Problema de dependencia


La documentación para REST Assured indica que para usar Oauth1 u Oauth2 (especificando un token como parámetro de consulta), se deben agregar autorizaciones según el Scribe. Sin embargo, importar la última versión no lo ayudará; encontrará un error descrito en uno de los problemas abiertos . Solo puede resolverlo importando la versión anterior de la biblioteca, 2.5.3. Sin embargo, en este caso te encontrarás con otro problema .


En general, ninguna otra versión de Scribe funciona con Oauth2 REST Assured versión 3.0.3 y superior (y la versión reciente 4.0.0 no solucionó esto).


El registro no funciona


Los filtros se aplican a las consultas en un orden específico. Y AuthenticationScheme se aplica después de ellos. Esto significa que será difícil detectar un problema con la autorización en la prueba; no está comprometido.


Más acerca de la sintaxis asegurada REST


Una gran cantidad de pruebas generalmente significa que también son complejas. Y si la API es el tema principal de las pruebas, y necesita verificar no solo los campos json, sino también la lógica de negocios, entonces con REST Assured la prueba se convierte en una hoja:


 @Test public void shouldCorrectlyCountAddedCookies() { Integer addNumber = 10; JsonPath beforeCookies = given() .when() .get("/latestcookies") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); String beforeId = beforeCookies.getString("id"); JsonPath afterCookies = given() .body(String.format("{number: %s}", addNumber)) .when() .put("/cookies") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); Integer afterNumber = afterCookies.getInt("number"); String afterId = afterCookies.getString("id"); JsonPath history = given() .when() .get("/history") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); assertThat(history.getInt(String.format("records.find{r -> r.id == %s}.number", beforeId))) .isEqualTo(afterNumber - addNumber); assertThat(history.getInt(String.format("records.find{r -> r.id == %s}.number", afterId))) .isEqualTo(afterNumber); } 

Esta prueba verifica que cuando alimentamos una cookie monstruosa, calculamos correctamente cuántas cookies se le dieron e indicamos esto en la historia. Pero a primera vista esto no se puede entender: todas las solicitudes tienen el mismo aspecto, y no está claro dónde termina la preparación de datos a través de la API y dónde se envía la solicitud de prueba.


given() , when() y then() REST Assured toma de BDD, como Spock o Cucumber. Sin embargo, en pruebas complejas, su significado se pierde, porque la escala de la prueba se vuelve mucho más grande que una solicitud; esta es una pequeña acción que debe denotarse con una línea. Y para esto, puede transferir llamadas REST Assured a clases auxiliares:


 public class CookieMonsterHelper { public static JsonPath getCookies() { return given() .when() .get("/cookiesformonster") .then() .extract() .jsonPath(); } ... } 

Y llame en la prueba:


 JsonPath response = CookieMonsterHelper.getCookies(); 

Es bueno cuando tales clases auxiliares son universales para que una llamada a un método pueda integrarse en una gran cantidad de pruebas; luego, se pueden colocar en una biblioteca separada en general: de repente, debe llamar al método en algún momento de otro proyecto. Solo en este caso tendrá que eliminar toda la verificación de la respuesta que Rest Assured puede hacer; después de todo, a menudo se pueden devolver datos muy diferentes en respuesta a la misma solicitud.


Conclusión


REST Assured es una biblioteca para pruebas. Ella sabe cómo hacer dos cosas: enviar solicitudes y verificar respuestas. Si intentamos eliminarlo de las pruebas y eliminar toda la validación, se convierte en un cliente HTTP .


Si tiene que escribir una gran cantidad de pruebas y continuar admitiéndolas, piense si necesita un cliente HTTP con una sintaxis engorrosa, una configuración estática, confusión en el orden de aplicación de filtros y especificaciones, y registros que se pueden romper fácilmente. Quizás hace nueve años, REST Assured era la herramienta más conveniente, pero durante este tiempo aparecieron alternativas (Retrofit, Feign, Unirest, etc.) que no tienen tales características.


La mayoría de los problemas que se describen en el artículo se manifiestan en grandes proyectos. Si necesita escribir rápidamente un par de pruebas y olvidarse de ellas para siempre, y a Retrofit no le gusta, REST Assured es la mejor opción.


Si ya está escribiendo pruebas con REST Assured, no es necesario apresurarse para reescribir todo. Si son estables y rápidos, pasará más tiempo que beneficios prácticos. Si no, REST Assured no es su principal problema.


Todos los días, el número de pruebas escritas en DINS para RingCentral API está creciendo, y todavía usan REST Assured. La cantidad de tiempo que tendrá que pasar para cambiar a otro cliente HTTP, incluso en las nuevas pruebas, es demasiado grande, y las clases auxiliares y los métodos creados que configuran la configuración de la prueba resuelven la mayoría de los problemas. En este caso, mantener la integridad del proyecto con las pruebas es más importante que usar el cliente más bello y moderno. REST Assured, a pesar de sus defectos, hace su trabajo principal.

Source: https://habr.com/ru/post/464225/


All Articles