Test Data Generation Using Model Classes

When testing modern REST based microservices and systems REST-assured is a great tool for testing the RESTful endpoints from a java test framework. It provides a fluent structure for the requests that is easy to read. The structure of a test assured request is:

    given()
        .when()
            .contentType(ContentType.JSON)
            .body(payload)
            .post("/some/resource")
        .then()
            .statusCode(200)
            .extract()
            .response();
                        

However some of the approaches for supporting this structure don't have the same aesthetic appeal. In this case I am talking about the payloads that are created for the body of the request. I have often seen where JSON requests are constructed as strings:

    private static String payload =
        "{\n" + "   \"id\": \"%s\",\n" +
        "   \"position\": \"13b7ebec-87bf-4c40-84bd-9c1d1a7a513a\",\n" +
        "   \"names\": [\n" +
        "       {\n" +
        "           \"name\": \"zQwWbQXP\",\n" +
        "           \"positions\": [\n" +
        "               \"boss\",\n" +
        "               \"manager\",\n" +
        "               \"grunt\"\n" +
        "           ],\n" +
        "           \"city\": \"Paris\"\n" +
        "       },\n" +
        "       {\n" +
        "           \"name\": \"PzeKelpT\",\n" +
        "           \"positions\": [\n" +
        "               \"boss\",\n" +
        "               \"manager\",\n" +
        "               \"grunt\"\n" +
        "           ],\n" +
        "           \"city\": \"%s\"\n" +
        "       }\n" +
        "   ]\n" +
        "}";
                        

In addition for dynamic values within the JSON string there could be methods to add them when they are being retrieved.

    public String getPayload(String id, String city) {
        return String.format(payload, id, city);
    }

    public String getPayload(String id) {
        return getPayload(id, “Rome”);
    }
                        

This approach is very messy and requires multiple methods for multiple parameters. It is also not very scalable or readable. If a new json or xml parameter is added the entire string needs to be changed and possible updates to/additional methods added.

A much better way (in most use cases) is the use of model classes to generate the payload. The body method in a REST-assured RequestSpecification has an implementation that takes in an object. REST-assured will serialize this object to a valid JSON payload when used in the request . Here is the way to accomplish this:


Model Class for JSON

The initial step in this process is to create the model class(es) that will be used to create the test payload. For this the java library Lombok is used to create a builder and to handle the getters, setters, toString and required arguments constructor:

    @Data
    @Builder
    @JsonDeserialize(builder = DataModel.DataModelBuilder.class)
    public class DataModel {
        @JsonProperty
        @Builder.Default
        String id = randomUUID().toString();
        @JsonProperty
        @Builder.Default
        String position = randomUUID().toString();
        @JsonProperty
        @Builder.Default
        List<NamesModel> names = Arrays.asList(NamesModel.builder().build(), NamesModel.builder().build());
    }
                        

In the above:

  • Data is a lombok annotation that creates the getters, setters, required args constructor and toString methods behind the scenes
  • Builder is a Lombok annotation to create the builder pattern in the background. This is useful for changing parts of the data for different test scenarios
  • JsonDeserialize is a Jackson annotation that allows the Jackson deserializer to use the builder for responses
  • JsonProperty is a Jackson annotation that marks the property as a json property and can be used to name the property differently when it's turned into json for the payload. If, as in the case above, a name is not provided the default name is used. An example of this would be in headers where: @JsonProperty("Content-Type") would be used as - is not allowed in field names in java. In the above case it is not required
  • Builder.Default sets the default value for the field so that if you don't set it specifically it will use the default value

**Note in the above the 'names' field is a list of type NamesModel which is another model class.

For the sake of completeness here is the NamesModel class (also a Lombok builder):

    @Data
    @Builder
    @JsonDeserialize(builder = NamesModel.NamesModelBuilder.class)
    public class NamesModel {
        @JsonProperty
        @Builder.Default
        String name = generateRandomString();
        @JsonProperty
        @Builder.Default
        List<String> positions = Arrays.asList("boss", "manager", "grunt");
        @JsonProperty
        @Builder.Default
        String city = generateRandomCity();
    }
                        

Here is it called:

    given()
        .when()
            .contentType(ContentType.JSON)
            .body(DataModel.builder().build())
            .post("/some/resource")
        .then()
            .statusCode(200)
            .extract()
            .response();
                        

You simply 'build' the payload and pass it into the body. The advantage of this method is you can simply add or remove fields if the payload changes so it is more scalable and efficient for testing purposes. You can use randomisers in the fields in the models to ensure you have different values every time. You can also change the values passed in if you want definite values using:

    DataModel.builder()
        .id(someIdValue)
        .position(somePosition)
        .build()
                        

Model Class for XML

XML can also be generated for REST requests using the same approach as JSON. The same model classes can be slightly modified to create both XML and JSON payloads for POST, PATCH and PUT requests. For this the DataModel class referenced above would be change as follows:

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonDeserialize(builder = DataModel.DataModelBuilder.class)
    @XmlAccessorType(XmlAccessType.FIELD)
    public class DataModel {
        @XmlAttribute
        @JsonProperty
        @Builder.Default
        String id = randomUUID().toString();
        @JsonProperty
        @Builder.Default
        String position = randomUUID().toString();
        @JsonProperty
        @Builder.Default
        List<NamesModel> names = Arrays.asList(NamesModel.builder().build(), NamesModel.builder().build());
    }
                        

The differences are in the addition of the NoArgsConstructor and AllArgsConstructor annotations and the XmlAccessorType annotation (which specifies whether fields or properties should be serialized - for model classes it should be set to fields).

You can also (it is not required) add an XmlAttribute annotation to any of the fields in the model class. This has the effect of making that field an XML attribute rather than an XML field in the payload. The NamesModel class can remain as is.

The changes need only be made to the main model class. The XmlAccessorType is inherited. With the above you are set to create an XML payload for your REST-assured driven test. In order to turn the above model class into a payload you need to 'marshall it'. Here is how:

    StringWriter sw = new StringWriter();
    JAXBContext jaxbContext = JAXBContext.newInstance(DataModel.class);
    Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
    jaxbMarshaller.setProperty(JAXB_FORMATTED_OUTPUT, TRUE);

    QName qName = new QName(XML_NS, XML_LOCAL_PART);
    JAXBElement<DataModel> root = new JAXBElement<>(qName, DataModel.class, dataFromModel);
    jaxbMarshaller.marshal(root, sw);
    String payload = sw.toString();
                        

It can then be called like so:

    given()
        .when()
            .contentType(ContentType.XML)
            .body(payload)
            .post("/some/resource")
        .then()
            .statusCode(200)
            .extract()
            .response();
                        

Model Class for Headers

This makes the solution very flexible. REST-assured also has a headers method that takes in a key-value hash. This can be achieved with the model classes using Jackson's ObjectMapper like so:

    ObjectMapper objectMapper = new ObjectMapper();
    Map<String, String> headersFromModel = objectMapper.convertValue(HeadersModel.builder().build(), new TypeReference<Map<String, String>>() { });
                        

In the above headersFromModel can be passed into the headers method of the REST-assured method chain. This enables the tester to put in negative and positive header values to fully test the headers and their effect on the request.


Model Class for Responses

It is also possible to use a model class for the response and extract it like so:

    ResponseClass response = ...
        .then()
        .extract()
        .response()
        .as(ResponseClass.class)
                        

The above way of capturing the response allows the user to use the getters of the response class to check the correctness of the returned response. This is also particularly useful in a negative test situation where the response is expected to be an error and the error structure and fields can be verified for correctness and structure.


Validation Constraints within the Model Response Class

Validation annotations on the fields of the Response class can be used to validate the response during the binding of the response to the model. This extra layer of validation requires very little effort and is very efficient.

    @Data
    public class MyResponse {
        @NotBlank
        @Size(min=1, max=16)
        String id;
        @NotNull
        String name;
        @Past
        Date birthday;
        @Email
        String email;
        @Pattern(regexp="\\(\\d{3}\\)\\d{3}-\\d{4}")
        String phoneNumber;
    }
                        

In the above:

  • @NotBlank means the element must not be null and must contain at least one non-whitespace character
  • @NotNull means the element must not be null
  • @Size means the element size must be between the specified boundaries
  • @Past means the element must be an instant, date or time in the past
  • @Email means the element has to be a well-formed email address
  • @Pattern means the element character sequence must match the specified regular expression

There are many more validation constraints that can be put on response model classes. The full list can be gotten from the javadoc