Slide 1

Slide 1 text

How to fast generate your API Test with OpenAPI Tools and Rest-Assured Elias Nogueira Short version

Slide 2

Slide 2 text

ELIAS NOGUEIRA Senior Principal Software Engineer Brazil Java Champion Oracle ACE for Java Java Magazine NL Editor

Slide 3

Slide 3 text

preconditions

Slide 4

Slide 4 text

For this talk, you must know about: ● Java (intermediate) ● Maven (intermediate) ● REST-Assured (advanced) ● OpenAPI Specs (understand the spec) ● Architecture (intermediate) preconditions eliasnogueira.com

Slide 5

Slide 5 text

generating the code

Slide 6

Slide 6 text

generating the code eliasnogueira.com wagon-maven-plugin openapi-spec openapi generator maven-plugin java code download is used by generate

Slide 7

Slide 7 text

download the spec file The wagon-maven-plugin will help us to download a file to a specific directory. In general, we will tell the plugin to: ○ download a file ○ describe the file location ○ describe the destination eliasnogueira.com

Slide 8

Slide 8 text

download the spec file eliasnogueira.com org.codehaus.mojo wagon-maven-plugin ${wagon-maven-plugin.version} download-credit-api-spec download-single generate-sources URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION ${project.basedir}/target/openapiSpecs

Slide 9

Slide 9 text

download the spec file eliasnogueira.com org.codehaus.mojo wagon-maven-plugin ${wagon-maven-plugin.version} download-credit-api-spec download-single generate-sources URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION ${project.basedir}/target/openapiSpecs unique id to identify the execution action, in case of multiple downloads

Slide 10

Slide 10 text

download the spec file eliasnogueira.com org.codehaus.mojo wagon-maven-plugin ${wagon-maven-plugin.version} download-credit-api-spec download-single generate-sources URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION ${project.basedir}/target/openapiSpecs goal from the plugin

Slide 11

Slide 11 text

download the spec file eliasnogueira.com org.codehaus.mojo wagon-maven-plugin ${wagon-maven-plugin.version} download-credit-api-spec download-single generate-sources URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION ${project.basedir}/target/openapiSpecs Maven Build Lifecycle that will trigger this action

Slide 12

Slide 12 text

download the spec file eliasnogueira.com org.codehaus.mojo wagon-maven-plugin ${wagon-maven-plugin.version} download-credit-api-spec download-single generate-sources URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION ${project.basedir}/target/openapiSpecs URL to the file

Slide 13

Slide 13 text

download the spec file eliasnogueira.com org.codehaus.mojo wagon-maven-plugin ${wagon-maven-plugin.version} download-credit-api-spec download-single generate-sources URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION ${project.basedir}/target/openapiSpecs file internal location

Slide 14

Slide 14 text

generate the Client API The openapi-generator-maven-plugin will help us to generate the Client API and it models based on the Open API file specification. In general, we will tell the plugin to: ○ look at a specific folder to know the spec file ○ define the main, api and model packages ○ use REST Assured as a support library ○ set the serialization library eliasnogueira.com

Slide 15

Slide 15 text

generate the Client API eliasnogueira.com generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson

Slide 16

Slide 16 text

generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson generate the Client API eliasnogueira.com unique id in case of many generator

Slide 17

Slide 17 text

generate the Client API eliasnogueira.com generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson goal to generate the code

Slide 18

Slide 18 text

generate the Client API eliasnogueira.com generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson Maven Lifecycle phase that will trigger the execution

Slide 19

Slide 19 text

generate the Client API eliasnogueira.com generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson Open API spec file location

Slide 20

Slide 20 text

generate the Client API eliasnogueira.com generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson the package used for the generated invoker (common) objects

Slide 21

Slide 21 text

generate the Client API eliasnogueira.com generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson the package used for the generated client api

Slide 22

Slide 22 text

generate the Client API eliasnogueira.com generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson the package used for the generated models

Slide 23

Slide 23 text

generate the Client API eliasnogueira.com generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson other configurations

Slide 24

Slide 24 text

generate the Client API eliasnogueira.com generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson library used in the client api

Slide 25

Slide 25 text

generate the Client API eliasnogueira.com generate-client-api-code generate generate-sources ${project.build.directory}/openapiSpecs/credit-api.yaml com.eliasnogueira.credit.invoker com.eliasnogueira.credit.api com.eliasnogueira.credit.model java false false rest-assured jackson serialization library

Slide 26

Slide 26 text

code generated

Slide 27

Slide 27 text

code generated The code is generated at the target/generated-sources/openapi folder. There will be three main packages: ● api: client api code ● invoker: class specifics for the target library (REST-Assured) ● model: all models from the spec eliasnogueira.com

Slide 28

Slide 28 text

code generated The most important code generated are the Api ones. They have all the API operations from the OpenAPI spec ready to be used with REST-Assured as it has the request and response specifications. eliasnogueira.com public static class GetRestrictionV1Oper implements Oper { public static final Method REQ_METHOD = GET; public static final String REQ_URI = "/api/v1/restrictions/{cpf}"; private RequestSpecBuilder reqSpec; private ResponseSpecBuilder respSpec; public GetRestrictionV1Oper(RequestSpecBuilder reqSpec) { this.reqSpec = reqSpec; reqSpec.setAccept("application/json"); this.respSpec = new ResponseSpecBuilder(); } }

Slide 29

Slide 29 text

architecture

Slide 30

Slide 30 text

architecture Proposed architecture eliasnogueira.com *Test RestApiClientBuilder *ApiClient Client API abstraction *ApiService Service abstraction build use use

Slide 31

Slide 31 text

architecture *ApiClient This will abstract the current Client API class generated by the OpenAPI Generator, based on REST Assured, and will use the RestClientApiBuilder to add the common request specification. The generated Client Api class adds an inner class per HTTP request matching the OpenAPI spec. eliasnogueira.com

Slide 32

Slide 32 text

architecture eliasnogueira.com public class RestrictionsApi { public static RestrictionsApi restrictions(Supplier reqSpecSupplier) { return new RestrictionsApi(reqSpecSupplier); } public OneUsingGETOper oneUsingGET() { return new OneUsingGETOper(createReqSpec()); } }

Slide 33

Slide 33 text

architecture eliasnogueira.com public class RestrictionsApi { public static RestrictionsApi restrictions(Supplier reqSpecSupplier) { return new RestrictionsApi(reqSpecSupplier); } public OneUsingGETOper oneUsingGET() { return new OneUsingGETOper(createReqSpec()); } } Will be used by the RestClientApiBuilder to build the Client Api

Slide 34

Slide 34 text

architecture eliasnogueira.com public class RestrictionsApi { public static RestrictionsApi restrictions(Supplier reqSpecSupplier) { return new RestrictionsApi(reqSpecSupplier); } public OneUsingGETOper oneUsingGET() { return new OneUsingGETOper(createReqSpec()); } } Inner class with the HTTP method, basePath and requests specifics (params)

Slide 35

Slide 35 text

architecture eliasnogueira.com public static class OneUsingGETOper implements Oper { public static final Method REQ_METHOD = GET; public static final String REQ_URI = "/api/v1/restrictions/{cpf}"; @Override public T execute(Function handler) { // magic } public static final String CPF_PATH = "cpf"; public OneUsingGETOper cpfPath(Object cpf) { reqSpec.addPathParam(CPF_PATH, cpf); return this; } }

Slide 36

Slide 36 text

architecture eliasnogueira.com public static class OneUsingGETOper implements Oper { public static final Method REQ_METHOD = GET; public static final String REQ_URI = "/api/v1/restrictions/{cpf}"; @Override public T execute(Function handler) { // magic } public static final String CPF_PATH = "cpf"; public OneUsingGETOper cpfPath(Object cpf) { reqSpec.addPathParam(CPF_PATH, cpf); return this; } } HTTP method and baseUri

Slide 37

Slide 37 text

architecture eliasnogueira.com public static class OneUsingGETOper implements Oper { public static final Method REQ_METHOD = GET; public static final String REQ_URI = "/api/v1/restrictions/{cpf}"; @Override public T execute(Function handler) { // magic } public static final String CPF_PATH = "cpf"; public OneUsingGETOper cpfPath(Object cpf) { reqSpec.addPathParam(CPF_PATH, cpf); return this; } } adds the HTTP method and baseUri to the request

Slide 38

Slide 38 text

architecture eliasnogueira.com public static class OneUsingGETOper implements Oper { public static final Method REQ_METHOD = GET; public static final String REQ_URI = "/api/v1/restrictions/{cpf}"; @Override public T execute(Function handler) { // magic } public static final String CPF_PATH = "cpf"; public OneUsingGETOper cpfPath(Object cpf) { reqSpec.addPathParam(CPF_PATH, cpf); return this; } } param name and method to add it into the request

Slide 39

Slide 39 text

architecture *ApiClient Creation We need to use the RestApiClientBuilder to build the Client Api instance to add the URL and port. Then we add a method per HTTP request. This is a recommended approach to ease any change (even to a different library). eliasnogueira.com

Slide 40

Slide 40 text

architecture RestApiClientBuilder The main necessity of this class is to add, to all requests, the baseUri, and the port. The basePath is not necessary as the generated client already has it. eliasnogueira.com public class RestApiClientBuilder { public T build(Function, T> clientCreator) { Supplier requestSpecBuilderSupplier = () -> new RequestSpecBuilder() .addRequestSpecification( new RequestSpecBuilder() .setBaseUri("http://localhost") .setPort(8088) .build()); return clientCreator.apply(requestSpecBuilderSupplier); } }

Slide 41

Slide 41 text

architecture eliasnogueira.com public class RestrictionsApiClient { private RestrictionsApi restrictionsApi = new RestApiClientBuilder().build(RestrictionsApi::restrictions); public Response queryCpf(String cpf) { return restrictionsApi.oneUsingGET().cpfPath(cpf).execute(Function.identity()); } } build of the Client Api using the RestApiClientBuilder

Slide 42

Slide 42 text

architecture eliasnogueira.com public class RestrictionsApiClient { private RestrictionsApi restrictionsApi = new RestApiClientBuilder().build(RestrictionsApi::restrictions); public Response queryCpf(String cpf) { return restrictionsApi.oneUsingGET().cpfPath(cpf).execute(Function.identity()); } } abstracting the internal (ugly) Client Api usage - returning a generic response - adding a meaningful name - adding the path parameter as the method parameter

Slide 43

Slide 43 text

architecture eliasnogueira.com public class RestrictionsApiClient { private RestrictionsApi restrictionsApi = new RestApiClientBuilder().build(RestrictionsApi::restrictions); public Response queryCpf(String cpf) { return restrictionsApi.oneUsingGET().cpfPath(cpf).execute(Function.identity()); } } using the auto-generated internal Client Api method

Slide 44

Slide 44 text

architecture eliasnogueira.com public class RestrictionsApiClient { private RestrictionsApi restrictionsApi = new RestApiClientBuilder().build(RestrictionsApi::restrictions); public Response queryCpf(String cpf) { return restrictionsApi.oneUsingGET().cpfPath(cpf).execute(Function.identity()); } } using the path parameter

Slide 45

Slide 45 text

architecture *ApiService The *ApiService abstraction will use the *ApiClient abstraction to consume its methods in different ways. This is the class we will use in the tests. The service can have one or multiple actions from the *ApiClient and it can be related to the Mediator design pattern, as it encapsulates how a set of objects (methods) interact. eliasnogueira.com

Slide 46

Slide 46 text

architecture *ApiService - Example We do have two test for the Restrictions API: ○ Expecting a restriction ○ Not expecting a restriction We will create the request for both, returning the correct response, in the service abstraction. eliasnogueira.com

Slide 47

Slide 47 text

architecture eliasnogueira.com public class RestrictionsApiService { private RestrictionsApiClient restrictionsApiClient = new RestrictionsApiClient(); /** * Query CPF without a restriction */ public boolean queryCpf(String cpf) { restrictionsApiClient.queryCpf(cpf).then().statusCode(HttpStatus.SC_NOT_FOUND); return true; } public MessageV1 queryCpfWithRestriction(String cpf) { return restrictionsApiClient.queryCpf(cpf).then(). statusCode(HttpStatus.SC_OK).extract().as(MessageV1.class); } }

Slide 48

Slide 48 text

architecture eliasnogueira.com public class RestrictionsApiService { private RestrictionsApiClient restrictionsApiClient = new RestrictionsApiClient(); /** * Query CPF without a restriction */ public boolean queryCpf(String cpf) { restrictionsApiClient.queryCpf(cpf).then().statusCode(HttpStatus.SC_NOT_FOUND); return true; } public MessageV1 queryCpfWithRestriction(String cpf) { return restrictionsApiClient.queryCpf(cpf).then(). statusCode(HttpStatus.SC_OK).extract().as(MessageV1.class); } } instance of the abstracted Client Api

Slide 49

Slide 49 text

architecture eliasnogueira.com public class RestrictionsApiService { private RestrictionsApiClient restrictionsApiClient = new RestrictionsApiClient(); /** * Query CPF without a restriction */ public boolean queryCpf(String cpf) { restrictionsApiClient.queryCpf(cpf).then().statusCode(HttpStatus.SC_NOT_FOUND); return true; } public MessageV1 queryCpfWithRestriction(String cpf) { return restrictionsApiClient.queryCpf(cpf).then(). statusCode(HttpStatus.SC_OK).extract().as(MessageV1.class); } } method to query the cpf expecting no restriction

Slide 50

Slide 50 text

architecture eliasnogueira.com public class RestrictionsApiService { private RestrictionsApiClient restrictionsApiClient = new RestrictionsApiClient(); /** * Query CPF without a restriction */ public boolean queryCpf(String cpf) { restrictionsApiClient.queryCpf(cpf).then().statusCode(HttpStatus.SC_NOT_FOUND); return true; } public MessageV1 queryCpfWithRestriction(String cpf) { return restrictionsApiClient.queryCpf(cpf).then(). statusCode(HttpStatus.SC_OK).extract().as(MessageV1.class); } } returning true because the status code is an HTTP 404 so, we can add an assertions in the test

Slide 51

Slide 51 text

architecture eliasnogueira.com public class RestrictionsApiService { private RestrictionsApiClient restrictionsApiClient = new RestrictionsApiClient(); /** * Query CPF without a restriction */ public boolean queryCpf(String cpf) { restrictionsApiClient.queryCpf(cpf).then().statusCode(HttpStatus.SC_NOT_FOUND); return true; } public MessageV1 queryCpfWithRestriction(String cpf) { return restrictionsApiClient.queryCpf(cpf).then(). statusCode(HttpStatus.SC_OK).extract().as(MessageV1.class); } } method to query the cpf expecting a restriction

Slide 52

Slide 52 text

architecture eliasnogueira.com public class RestrictionsApiService { private RestrictionsApiClient restrictionsApiClient = new RestrictionsApiClient(); /** * Query CPF without a restriction */ public boolean queryCpf(String cpf) { restrictionsApiClient.queryCpf(cpf).then().statusCode(HttpStatus.SC_NOT_FOUND); return true; } public MessageV1 queryCpfWithRestriction(String cpf) { return restrictionsApiClient.queryCpf(cpf).then(). statusCode(HttpStatus.SC_OK).extract().as(MessageV1.class); } } it returns the expected response body

Slide 53

Slide 53 text

architecture Test Now the tests will be created using only the Service class. The different is that we don’t need to use the raw REST Assured methods anymore, relying only in the Service. The structure of precondition, action and assert will be always present in the new way to create tests. eliasnogueira.com

Slide 54

Slide 54 text

architecture eliasnogueira.com @Test void shouldReturnRestriction() { given() .spec(SharedRequestSpecs.cpfPathParameter("62648716050")) .when() .get("/restrictions/{cpf}") .then() .statusCode(HttpStatus.SC_OK) .body("message", CoreMatchers.is("CPF 62648716050 has a restriction")); } Raw REST Assured Test

Slide 55

Slide 55 text

architecture eliasnogueira.com @Test void shouldReturnRestriction() { given() .spec(SharedRequestSpecs.cpfPathParameter("62648716050")) .when() .get("/restrictions/{cpf}") .then() .statusCode(HttpStatus.SC_OK) .body("message", CoreMatchers.is("CPF 62648716050 has a restriction")); } @Test void shouldReturnRestriction() { RestrictionsApiService restrictionsApiService = new RestrictionsApiService(); MessageV1 message = restrictionsApiService.queryCpfWithRestriction("60094146012"); Assertions.assertThat(message.getMessage()).contains("60094146012"); } Raw REST Assured Test REST Assured Test using Client – Service abstraction

Slide 56

Slide 56 text

architecture Test The usage of the service abstraction add more readability and help us to decrease the maintainability as we will have only one place to change it behaviour (client or service). eliasnogueira.com @Test void shouldReturnRestriction() { RestrictionsApiService restrictionsApiService = new RestrictionsApiService(); MessageV1 message = restrictionsApiService.queryCpfWithRestriction("60094146012"); Assertions.assertThat(message.getMessage()).contains("60094146012"); }

Slide 57

Slide 57 text

architecture eliasnogueira.com @Test void shouldReturnRestriction() { RestrictionsApiService restrictionsApiService = new RestrictionsApiService(); MessageV1 message = restrictionsApiService.queryCpfWithRestriction("60094146012"); Assertions.assertThat(message.getMessage()).contains("60094146012"); }

Slide 58

Slide 58 text

architecture eliasnogueira.com @Test void shouldReturnRestriction() { RestrictionsApiService restrictionsApiService = new RestrictionsApiService(); MessageV1 message = restrictionsApiService.queryCpfWithRestriction("60094146012"); Assertions.assertThat(message.getMessage()).contains("60094146012"); } instance to use the Service abstraction

Slide 59

Slide 59 text

architecture eliasnogueira.com @Test void shouldReturnRestriction() { RestrictionsApiService restrictionsApiService = new RestrictionsApiService(); MessageV1 message = restrictionsApiService.queryCpfWithRestriction("60094146012"); Assertions.assertThat(message.getMessage()).contains("60094146012"); } usage of the method in the service

Slide 60

Slide 60 text

architecture eliasnogueira.com @Test void shouldReturnRestriction() { RestrictionsApiService restrictionsApiService = new RestrictionsApiService(); MessageV1 message = restrictionsApiService.queryCpfWithRestriction("60094146012"); Assertions.assertThat(message.getMessage()).contains("60094146012"); } associating the correct return (response body)

Slide 61

Slide 61 text

architecture eliasnogueira.com @Test void shouldReturnRestriction() { RestrictionsApiService restrictionsApiService = new RestrictionsApiService(); MessageV1 message = restrictionsApiService.queryCpfWithRestriction("60094146012"); Assertions.assertThat(message.getMessage()).contains("60094146012"); } assertion using the response body object

Slide 62

Slide 62 text

architecture BaseApiConfiguration Within the new approach, the general configuration is now being applied using the RestClientApiBuilder class, as it created a common request specification for all the requests. We can either move the previous configurations from the BaseApiConfiguration class to the RestClientApiBuilder or continue to use the BaseApiConfiguration without the baseUri, basePath, and port. eliasnogueira.com

Slide 63

Slide 63 text

architecture Recommendation A better approach is to use the RestClientApiBuilder to deal only with the global request actions and the BaseApiConfiguration with the configurations related to the test. Both classes do different things, and they must have a single responsibility. eliasnogueira.com

Slide 64

Slide 64 text

THANK YOU! @eliasnogueira