Reasons to avoid RandomStringUtils for test data generation
October 10, 2021How to use the Factory design pattern to create browser instances: local and remote approach
June 19, 2022Introduction
One of my activities is to review the Quality Engineers’ technical assignment. We ask the candidate to automate at least 2 features in the API and Web layers.
Most of the candidates are delivering the API test automation solution using Java and Rest-Assured, which is the same combination we use. Of course, we are evaluating it based on the candidate’s experience in general which includes the whole testing experience, test automation experience, and libraries experience.
Rest-Assured is a well-known library to use for API test automation, simple to use, and fast. But I can see one gap in most of the solutions they delivered: the lack of knowledge about the library. It’s in different ways and one is unanimous: the use of either String
or HashMap
in the requests.
The intention here is to tell you which way you can have most of the professional benefits using Rest-Assured as I refer to it here as the recommended approach.
The test implementation
To not add some complexity here and stay focused on our main goal we will use the https://reqres.in/, a mock API ready to respond to any request for testing purposes.
As the goal is sent a request with an object with it, sometimes referred to as the payload object, we will use the following endpoint:
Request
Path: /api/users
Request body (payload)
{
"name": "morpheus",
"job": "leader"
}
Response
Status code: 201
Response Body (example)
{
"name": "morpheus",
"job": "leader",
"id": "909",
"createdAt": "2021-03-20T16:02:52.694Z"
}
Given the request body, we must send the name
and job
attributes, which are text (strings).
Working approaches
String concatenation (not recommended)
The string concatenation approach consists of creating a string with the same output as JSON.
Note that, on line 5, the String
user is a string concatenation of the JSON Object we must send into the request.
As we are using Java we must escape all the double quotes, as a string starts and ends with it.
The validation on lines 15 and 16 must be hardcoded as well because there is no way to retrieve the data sent because it is a string.
class PostTest {
@Test
void postUsingStringConcatenation() {
String user = "{\"name\":\"Elias\",\"job\":\"Principal Engineer\"}";
given().
contentType(ContentType.JSON).
body(user).
when().
post("/users").
then().
statusCode(HttpStatus.SC_CREATED).
body(
"name", is("Elias"),
"job", is("Principal Engineer"),
"id", notNullValue(),
"createdAt", notNullValue()
);
}
}
We can see clearly that this approach works but will give you a lot of maintenance and trouble. You must avoid it.
Advantages
- none
Disadvantages
- hard to read
- hard to maintain
- looks unprofessional
- does not allow easy object updates (properties or values)
Using a HashMap (not recommended, but acceptable)
You can see this approach described in the Rest-Assured documentation but it does not mean this is the best approach.
This approach consists of creating a HashMap
object to send it on the request.
As a HashMap
as a key-value map where the key is the attribute and the value is the attribute value, it seems a good candidate to use.
In line 5 we are using a Map
typing the attribute as String and the value as an Object
. This is not the case but you know that a JSON value can be an object.
On lines 6 and 7 we are adding the key (attribute name) and value (the value we want to send).
Behind the scene, the binding library will transform the HashMap
into a JSON object like this:
{ "name": "Elias", "job": "Principal Engineer" }
class PostTest {
@Test
void postUsingHashMap() {
Map<String, Object> user = new HashMap<>();
user.put("name", "Elias");
user.put("job", "Principal Engineer");
given().
contentType(ContentType.JSON).
body(user).
when().
post("/users").
then().
statusCode(HttpStatus.SC_CREATED).
body(
"name", is(user.get("name")),
"job", is(user.get("job")),
"id", notNullValue(),
"createdAt", notNullValue()
);
}
}
This approach is better than the String concatenation and looks more professional, but it’s not the best one…
Advantages
- Give you a clear vision of the attributes and values
- Can enable you to go further and create a class placing the objects, like a test data factory approach
Disadvantages
- It’s not flexible to create complex objects, as you need to create several
HashMap
- An update can confuse you as you need to set the value in the same key. E.g:
put("name", "New value")
- Any typo in the keys will lead to errors that might be hard to find. Eg:
put("name", "Elias")
as we have a typo on “o”
Recommended approach
Using the Object Mapping
Rest-Assured supports mapping Java objects to and from JSON and XML. This means you can create a Java object and Rest-Assured will try to match all the attributes in the class with either the request or response.
To be able to use it you need to explicitly add an object mapping library in your classpath. For JSON you need to have either Jackson, Jackson2, Gson, or Johnzon in the classpath, and for XML you need JAXB.
As we are sending information into the request let’s talk about first the Serialization.
Object Mapping Serialization
https://github.com/rest-assured/rest-assured/wiki/Usage#serialization
Rest-Assured will use one of the object mapping libraries you added to your classpath to bind the attributes from a Java object into the request body. We already know that we required, for this example, the name
and job
attributes to post a user. The Java object will look like this:
public class User {
private String name;
private String job;
public User(String name, String job) {
this.name = name;
this.job = job
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
}
In the test, we need first to create the User
object with the data we would like to use. You can see this on line 6.
Now we simply use the user
object into the body(object). It’s important to explicitly set the content type. You can see it in lines 9 and 10.
class PostTest {
@Test
void postUsingObjectMapping() {
User user = new User("Elias", "Principal Engineer");
given().
contentType(ContentType.JSON).
body(user).
when().
post("/users").
then().
statusCode(HttpStatus.SC_CREATED).
body(
"name", is(user.getNama()),
"job", is(user.getJob()),
"id", notNullValue(),
"createdAt", notNullValue()
);
}
}
Rest-Assured will do the magic when the body()
method uses an object where it will try to find the required request attribute names in the object used.
The request needs the name
and job
attributes, as text. As the User
object has the name
and job attributes as String (text) RestAssured will to the automatic value match. Both combinations of attribute and value match are called Object Serialization.
Important note
You might face the following exception, which is super self-explanatory:
java.lang.IllegalStateException: Cannot serialize object because no JSON serializer found in the classpath. Please put Jackson (Databind), Gson, Johnzon, or Yasson in the classpath.
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
at org.codehaus.groovy.reflection.CachedConstructor.invoke(CachedConstructor.java:72)
at org.codehaus.groovy.runtime.callsite.ConstructorSite$ConstructorSiteNoUnwrapNoCoerce.callConstructor(ConstructorSite.java:105)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallConstructor(CallSiteArray.java:59)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:263)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callConstructor(AbstractCallSite.java:277)
at io.restassured.internal.mapping.ObjectMapping.serialize(ObjectMapping.groovy:160)
at io.restassured.internal.mapping.ObjectMapping$serialize.call(Unknown Source)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:125)
at io.restassured.internal.RequestSpecificationImpl.body(RequestSpecificationImpl.groovy:751)
This will happen if you don’t add any library to serialize the objects. You need to explicitly add one of the following libraries:
It might be the case you are not using one of these libraries, or having a successful test execution. Some dependencies can have one of these as an internal dependency, and you can figure out it simply by running mvn dependency:tree
and trying to look at one of the libraries mentioned.
Examples
In the restassured-complete-basic-example project, you can find the Simulation model, which models the request and response for the Simulations API.
The model is in use by the SimulaitonDataFactory to create the Simulation
data, necessary to make the POST
request using the body parameter.
The SimulationFunctionalTest has testes using different HTTP methods. You can see the createNewSimulationSuccessfully, invalidSimulations, and simulationWithDuplicatedCpf test methods using the Simulation
object to POST
a body using it instead of Strings or any other mentioned approach.
2 Comments
Hi Elias,
Thank you for this great blog. What do you recommend for Typescript/Javascript API resting? There is no library like rest assured.
Thanks
Hi Omer,
Unfortunately, I don’t know very well the JS/TS tools but I saw people using frisby.js