Share
How to fast generate your API Test with OpenAPI Tools and Rest-Assured
The creation of API Test Automation using Java and Rest-Assured might be boring because it always follows the same repeated pattern: creating the code to make the HTTP requests, the model classes used in the responses, the object mapping, and the tests.
With the usage of the OpenAPI Tools, we can quickly generate the client code and the test stubs, speeding up the development process.
You will also learn the possible problems during its adoption that can add more maintenance effort and how to solve it with abstractions.
You will learn how to use an OpenAPI specification to automatically generate the client code to use in your test, as we will call it basic usage. As the first improvement, you will learn how to use templates and automatically create custom client and test code. And as the ultimate improvement, you will learn how to decrease the maintenance and focus on a more business-driven development by creating abstractions on top of the client code
Elias Nogueira
Transcript
79 slides
-
How to fast generate your API Test with OpenAPI Tools
and Rest-Assured Elias Nogueira -
ELIAS NOGUEIRA Senior Principal Software Engineer Brazil Java Champion Oracle
ACE Java Magazine NL Editor -
The backend is composed of two services: Simulations API CRUD
operation to record a loan simulation Restrictions API GET endpoint to know if a consumer has a restriction, if yes, a simulation is not allowed. the backend application eliasnogueira.com -
the backend application eliasnogueira.com POST /simulations GET /restrictions has restriction?
HTTP 403 Forbidden Restrictions Simulations -
REST Assured http://rest-assured.io Java DSL for simplifying testing of REST
based services. It has an intuitive code syntax to create an automated test. Let’s express it in a natural language: Given I have a parameter to send When I send a request using an HTTP Method an URL Then I can validate the status code and response body NOTE: this is not a BDD approach and I have no intention to apply it eliasnogueira.com -
REST Assured http://rest-assured.io REST Assured uses the same keywords given,
when, then to express it import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; class RestAssuredExampleTest { @Test void welcomeTest() { given(). param("name", "Elias"). when(). post("/register"). then(). statusCode(200). body("message", is("Hello Elias")); } } eliasnogueira.com -
import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; class RestAssuredExampleTest { @Test
void welcomeTest() { given(). param("name", "Elias"). when(). post("/register"). then(). statusCode(200). body("message", is("Hello Elias")); } } REST Assured http://rest-assured.io import libraries REST Assured uses the same keywords given, when, then to express it eliasnogueira.com -
import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; class RestAssuredExampleTest { @Test
void welcomeTest() { given(). param("name", "Elias"). when(). post("/register"). then(). statusCode(200). body("message", is("Hello Elias")); } } REST Assured http://rest-assured.io test method and annotation from JUnit 5 REST Assured uses the same keywords given, when, then to express it eliasnogueira.com -
import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; class RestAssuredExampleTest { @Test
void welcomeTest() { given(). param("name", "Elias"). when(). post("/register"). then(). statusCode(200). body("message", is("Hello Elias")); } } REST Assured http://rest-assured.io request pre-condition REST Assured uses the same keywords given, when, then to express it eliasnogueira.com -
import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; class RestAssuredExampleTest { @Test
public void welcome() { given(). param("name", "Elias"). when(). post("/register"). then(). statusCode(200). body("message", is("Hello Elias")); } } REST Assured http://rest-assured.io action (request) REST Assured uses the same keywords given, when, then to express it eliasnogueira.com -
import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; class RestAssuredExampleTest { @Test
void welcomeTest() { given(). param("name", "Elias"). when(). post("/register"). then(). statusCode(200). body("message", is("Hello Elias")); } } REST Assured http://rest-assured.io assert the response body REST Assured uses the same keywords given, when, then to express it eliasnogueira.com -
robust framework You have noticed, so far, we are using
the “raw” REST Assured usage through the methods given(), when(), and then(). They are a great start, but it won’t scale as the API and test grow. One of the possible approaches is to create an abstraction around the main REST Assured methods, having minimal work using the OpenAPI spec. eliasnogueira.com -
robust framework We are using the Open API spec, but
the UI version of it eliasnogueira.com -
robust framework Normally we can also have the file and
use it to generate the basic structure of the request and response. There’re several tools, and we will use the: ◦ wagon-maven-plugin: to download the spec ◦ openapi-generator-maven-plugin: to generate the API client based on the spec eliasnogueira.com -
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 -
download the spec file eliasnogueira.com <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>wagon-maven-plugin</artifactId> <version>${wagon-maven-plugin.version}</version> <executions>
<execution> <id>download-credit-api-spec</id> <goals> <goal>download-single</goal> </goals> <phase>generate-sources</phase> <configuration> <url> URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION </url> <toDir>${project.basedir}/target/openapiSpecs</toDir> </configuration> </execution> </executions> </plugin> -
download the spec file eliasnogueira.com <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>wagon-maven-plugin</artifactId> <version>${wagon-maven-plugin.version}</version> <executions>
<execution> <id>download-credit-api-spec</id> <goals> <goal>download-single</goal> </goals> <phase>generate-sources</phase> <configuration> <url> URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION </url> <toDir>${project.basedir}/target/openapiSpecs</toDir> </configuration> </execution> </executions> </plugin> unique id to identify the execution action, in case of multiple downloads -
download the spec file eliasnogueira.com <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>wagon-maven-plugin</artifactId> <version>${wagon-maven-plugin.version}</version> <executions>
<execution> <id>download-credit-api-spec</id> <goals> <goal>download-single</goal> </goals> <phase>generate-sources</phase> <configuration> <url> URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION </url> <toDir>${project.basedir}/target/openapiSpecs</toDir> </configuration> </execution> </executions> </plugin> goal from the plugin -
download the spec file eliasnogueira.com <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>wagon-maven-plugin</artifactId> <version>${wagon-maven-plugin.version}</version> <executions>
<execution> <id>download-credit-api-spec</id> <goals> <goal>download-single</goal> </goals> <phase>generate-sources</phase> <configuration> <url> URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION </url> <toDir>${project.basedir}/target/openapiSpecs</toDir> </configuration> </execution> </executions> </plugin> Maven Build Lifecycle that will trigger this action -
download the spec file eliasnogueira.com <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>wagon-maven-plugin</artifactId> <version>${wagon-maven-plugin.version}</version> <executions>
<execution> <id>download-credit-api-spec</id> <goals> <goal>download-single</goal> </goals> <phase>generate-sources</phase> <configuration> <url> URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION </url> <toDir>${project.basedir}/target/openapiSpecs</toDir> </configuration> </execution> </executions> </plugin> URL to the file -
download the spec file eliasnogueira.com <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>wagon-maven-plugin</artifactId> <version>${wagon-maven-plugin.version}</version> <executions>
<execution> <id>download-credit-api-spec</id> <goals> <goal>download-single</goal> </goals> <phase>generate-sources</phase> <configuration> <url> URL-TO-THE-FILE-INCLUDING-FILE-NAME-AND-EXTENSION </url> <toDir>${project.basedir}/target/openapiSpecs</toDir> </configuration> </execution> </executions> </plugin> file internal location -
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 -
generate the Client API eliasnogueira.com <executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal>
</goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml </inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> -
<executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal> </goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml
</inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> generate the Client API eliasnogueira.com unique id in case of many generator -
generate the Client API eliasnogueira.com <executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal>
</goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml </inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> goal to generate the code -
generate the Client API eliasnogueira.com <executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal>
</goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml </inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> Maven Lifecycle phase that will trigger the execution -
generate the Client API eliasnogueira.com <executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal>
</goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml </inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> Open API spec file location -
generate the Client API eliasnogueira.com <executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal>
</goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml </inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> the package used for the generated invoker (common) objects -
generate the Client API eliasnogueira.com <executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal>
</goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml </inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> the package used for the generated client api -
generate the Client API eliasnogueira.com <executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal>
</goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml </inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> the package used for the generated models -
generate the Client API eliasnogueira.com <executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal>
</goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml </inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> other configurations -
generate the Client API eliasnogueira.com <executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal>
</goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml </inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> library used in the client api -
generate the Client API eliasnogueira.com <executions> <execution> <id>generate-client-api-code</id> <goals> <goal>generate</goal>
</goals> <phase>generate-sources</phase> <configuration> <inputSpec> ${project.build.directory}/openapiSpecs/credit-api.yaml </inputSpec> <invokerPackage>com.eliasnogueira.credit.invoker</invokerPackage> <apiPackage>com.eliasnogueira.credit.api</apiPackage> <modelPackage>com.eliasnogueira.credit.model</modelPackage> <generatorName>java</generatorName> <generateApiTests>false</generateApiTests> <generateModelTests>false</generateModelTests> <configOptions> <library>rest-assured</library> <serializationLibrary>jackson</serializationLibrary> </configOptions> </configuration> </execution> </executions> serialization library -
architecture Using the raw features of REST Assured for test
creation might not scale as we need to solve possible duplication along its creation. The changes in the API spec will also influence, increasing the code maintainability. Thankfully, using abstractions and design patterns we can easily solve these problems. eliasnogueira.com -
architecture Proposed architecture eliasnogueira.com *Test *Test *Data RestApiClientBuilder *ApiClient *ApiService
Client API abstraction Service abstraction build uses use -
architecture RestApiClientBuilder Each Client API generated by the OpenAPI Generator
will carry the full path to the related HTTP request. SimulationsApi.java eliasnogueira.com public static class DeleteUsingDELETEOper implements Oper { public static final Method REQ_METHOD = DELETE; public static final String REQ_URI = "/api/v1/simulations/{cpf}"; // code ignored -
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> T build(Function<Supplier<RequestSpecBuilder>, T> clientCreator) { Supplier<RequestSpecBuilder> requestSpecBuilderSupplier = () -> new RequestSpecBuilder() .addRequestSpecification( new RequestSpecBuilder() .setBaseUri("http://localhost") .setPort(8088) .build()); return clientCreator.apply(requestSpecBuilderSupplier); } } -
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 -
architecture eliasnogueira.com public class RestrictionsApi { public static RestrictionsApi restrictions(Supplier<RequestSpecBuilder>
reqSpecSupplier) { return new RestrictionsApi(reqSpecSupplier); } public OneUsingGETOper oneUsingGET() { return new OneUsingGETOper(createReqSpec()); } } -
architecture eliasnogueira.com public class RestrictionsApi { public static RestrictionsApi restrictions(Supplier<RequestSpecBuilder>
reqSpecSupplier) { return new RestrictionsApi(reqSpecSupplier); } public OneUsingGETOper oneUsingGET() { return new OneUsingGETOper(createReqSpec()); } } Will be used by the RestClientApiBuilder to build the Client Api -
architecture eliasnogueira.com public class RestrictionsApi { public static RestrictionsApi restrictions(Supplier<RequestSpecBuilder>
reqSpecSupplier) { return new RestrictionsApi(reqSpecSupplier); } public OneUsingGETOper oneUsingGET() { return new OneUsingGETOper(createReqSpec()); } } Inner class with the HTTP method, basePath and requests specifics (params) -
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> T execute(Function<Response, T> handler) { // magic } public static final String CPF_PATH = "cpf"; public OneUsingGETOper cpfPath(Object cpf) { reqSpec.addPathParam(CPF_PATH, cpf); return this; } } -
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> T execute(Function<Response, T> 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 -
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> T execute(Function<Response, T> 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 -
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> T execute(Function<Response, T> 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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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); } } -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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"); } -
architecture eliasnogueira.com @Test void shouldReturnRestriction() { RestrictionsApiService restrictionsApiService = new
RestrictionsApiService(); MessageV1 message = restrictionsApiService.queryCpfWithRestriction("60094146012"); Assertions.assertThat(message.getMessage()).contains("60094146012"); } -
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 -
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 -
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) -
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 -
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 -
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