Understanding REST assured as a beginner and not only

Let’s start with the basics

Why don’t we just stop testing manually?

  • After all, this saves time. This is obvious, because running an autotest will take a few seconds, or even fractions of seconds, and manually – tens of seconds, or even several minutes.

  • Autotest will automatically generate documentation and reporting for us.

  • Then autotest will allow us to reduce the impact of the human factor. Where an autotest that is properly configured will always pass without errors, a person can make a mistake.

  • High level of accuracyof course, compared to manual testing.

It turns out that autotests are a panacea that needs to be implemented voluntarily-compulsorily? This is far from true.

Under what conditions do we NOT need REST API test automation?

Case #1 – Early Development Stages.

When we have just received a still raw API and it continues to develop in the process, it is better to test it manually until it finds some kind of stable version.

There are, of course, exceptions. There are cases when the documentation has already been written and, in principle, the API will not change further. Then, of course, you can start writing autotests in parallel with the work of the developer who writes this API. But it seems to me that such situations are still rare.

Case #2 – tests that cannot be fully automated.

For example, when part of the test is run manually, and part of the test is automated.

  • This does not give us any time savings, by and large.

  • Tests become more difficult to maintain.

  • And their credibility is questionable.

Why do we need API autotests then?

No. 1. They allow us to simplify the localization of the bug. When we test through the UI, we first need to localize the bug, understand where it is at all. When we test through the API, we already understand where it is.

No. 2. The reduction in the cost of finding a bug is achieved due to the fact that we testing at the API level, not at the UI level.

No. 3. AND compliance check in terms of reliability, security and performance.

So what do we need to know about the API we’re testing?

No. 1. First, let’s make sure that the API looks at the right stand and database. What’s the point of irrelevant data? Let even the test passed, but he looks in the wrong direction.

No. 2. Then we check the correctness of the HTTP status entry.

No. 3. Check the payload of the response. Here we need to check the correctness of the JSON body: the names, types and value of the response fields.

No. 4. Checking response headers. They affect both security and performance.

No. 5. And we check the basic performance. If the operation completed successfully but took too long, the test failed.

Consequences

What mistakes can we make if we do not perform all these checks?

  • Outdated test results. When we look, for example, at the wrong database or at the wrong stand, we get outdated information.

  • Skip bugsincluding the critical ones.

  • Security non-compliance and performance. The user may be defenseless against an attacker, which, of course, we do not need. The user may simply not wait for the target action to be performed or may not be able to perform it at all.

What types of test scripts do we use?

  • Major positive tests is the default success scenario.

  • Extended positive testingwhere we add additional parameters.

  • And negative testing. Here we check for both valid input and invalid input.

I would also like to note that it is always worth clarifying. Colleagues with little experience and those who are just planning to start working in testing, I appeal to you. If something is not clear to the end – it is better to clarify once again. Colleagues with a lot of experience, please, please answer these questions.

HTTP status codes

Since we talked about what we get in responses, let’s remember the HTTP status codes.

1хх – Informational. Hundredth information codes tell us that the request has been received and processing continues.

2хх – Success. The 200 status codes indicate that the request was received, successfully processed, and understood.

3xx – Redirection. 300 response codes indicate that more actions are required to fulfill the request.

4хх – Client Error: requests have bad syntax or cannot be executed.

5xx – The server is unable to fulfill the invalid request.

Since we are talking about testing exactly the REST API, let’s remember: what is REST.

What is REST?

REST or Representational State Transfer is an architectural style that describes the interaction of distributed application components within a network. In other words, this is a set of restrictions that enable us to scale the application in the future.

RESTfull is the compliance of a web service with REST requirements.

REST-architecture

REST-architecture

Now we have already smoothly approached the very essence of the article, so we will highlight …

Four main REST-assured methods

  • Given – allows you to find out what was transferred in the request.

  • When – with which method and to which endpoint we send the request.

  • Then – how the received response is checked.

  • Body – the body, in our case, the response.

REST-assured is generally an API testing tool, and it is built into Java tests. And here is a specific example, or rather a diagram of how it looks.

given()...
  .when()
  .get(someEndpoint)
  .then()
  .statusCode(200)
  .body();

It is also worth using the Builder pattern.

Pattern Builder

For what? It allows you to make the code clearer by adding a hint of what can be configured using this same pattern.

Pattern example.

Account account = new AccountBuilder()
                .withId(1)
                .withName("test")
                .withBalance(10)
                .build();

If we pay attention, we will notice that REST-assured correspond to the Builder pattern.

Let’s go to an example

What do we need to prepare?

In this case, we will be testing an API that accepts office search parameters and returns a list of information about each office as a result.

We will use the following stack: Java API, REST-assured library and jUnit5, and Builder pattern.

We take into account all the features of the project

For example, in our case, the peculiarity is that we do not know what is happening under the hood, since we are accessing a private service. We have to be content only with the answer that he returns to us.

It would be possible to register the port directly in the URL.

BASE_URL = "http://alfaххх-intХ:ХХХХ/ххххххх-ххххх-ххх-хххх-api"

But instead of a hardcoded port, a method was written that goes to the orchestrator and gets the actual port and instance.

public class MarathonUtils {
    private static final String protocol = "http";

    public static String getServiceBaseUrl(String serviceMarathonPath) {

        Response response = RestAssured.given()
                .auth().basic(getPropertyOfName("mLogin"), getPropertyOfName("mPassword"))
                .baseUri(protocol + getPropertyOfName("mUri"))
                .basePath(serviceMarathonPath)
                .get("?embed=app.taskStats&embed=app.readiness")
                .then()
                .statusCode(200)
                .extract().response();
        //Возвращает первый найденный объект с информацией о работающем инстансе.
        Map<String, ?> state = response.getBody().path("app.tasks.find { it.state == 'TASK_RUNNING' }");

        if (state != null) {
            String host = state.get("host").toString();
            @SuppressWarnings("unchecked")
            ArrayList<String> ports = (ArrayList<String>) state.get("ports");
            String uri = response.getBody().path("app.env.SERVICE_NAME").toString();
            return String.format("%s://%s:%s/%s", protocol, host, ports.get(0), uri);
        }

        throw new NullPointerException("Отсутствуют работающие инстансы для " + serviceMarathonPath);
    }
}

Next, we compose JSON-schema according to the body of the API response

Let’s take a look at her. This is a schema fragment that is used to test this project and this API.

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "array",
  "items": [
    {
      "type": "object",
      "properties": {
        "pid": {
          "type": "string"
        },
        "uuid": {
          "type": "string"
        },

It is desirable, of course, to write the circuit yourself, but you can also use free tools on the Internet. After that, be sure to check and correct the generated circuit.

What does the schema check?

It checks the data type. Depending on the type of data, additional rules may apply. For example, for the Integer data type, you can check the minimum-maximum number, for the Array array – the maximum number of elements, the mandatory elements (required), and so on.

But the example of the scheme is more complicated, more detailed and more informative.

{ 
    "$schema": "http://json-schema.org/draft-03/schema#", 
    "id": "urn:product_name#", 
    "type": "string", 
    "pattern":     "^\\S.*\\S$", 
    "minLength": 3, 
    "maxLength": 50, 
}

Note: JSON-schema is used because it’s much easier to validate against it than each individual parameter. The API gives us information in a large volume, why not use JSON-schema, in which we can write absolutely everything?

Let’s write an autotest for a basic check

@ExtendWith(ReportPortalExtension.class)
public class BaseTest {
    static RequestSpecification requestSpecification;


    @BeforeAll
    static void setUp() {
        requestSpecification = RestAssured.given()
                .baseUri(BASE_URL_POS)
                .accept(ContentType.JSON);
    }

    public static ValidatableResponse checkStatusCodeInResponse(String url, int code, String schema)
    {
        return RestAssured.given(requestSpecification)
                .get(url)
                .then()
                .statusCode(code)
                .body(matchesJsonSchemaInClasspath(schema))
                .time(lessThan(1500L));
    }

    public void baseNegativeCheck(String posUrl) {
        checkStatusCodeInResponse(posUrl, CLIENT_ERROR_CODE, POS_API_SCHEMA_ERROR_JSON);
    }

    public void basePositiveHasItemsCheck(String path, String item, String posUrl, String schema) {
        checkStatusCodeInResponse(posUrl, SUCCESS_CODE, schema)
                .body(path, hasItems(item));
    }

    public void hasSizeCheck(String url, String path, int size) {
        checkStatusCodeInResponse(url, SUCCESS_CODE, POS_API_SCHEMA_EMPTY_JSON)
                .body(path, hasSize(size));
    }

    public void equalToCheck(String url, String path, String schema, boolean operand) {
        checkStatusCodeInResponse(url, SUCCESS_CODE, schema)
                .body(path, equalTo(operand));
    }

    public void positiveHasEntryCheck(String url, String path, String key, String value) {
        checkStatusCodeInResponse(url, SUCCESS_CODE, POS_API_SCHEMA_JSON)
                .body(path, everyItem(hasItem(hasEntry(key, value))));
    }

    public void checkEveryItemHasSize(String url, int size, String path) {
        checkStatusCodeInResponse(url, SUCCESS_CODE, POS_API_SCHEMA_EMPTY_JSON)
                .body(path, everyItem(hasSize(size)));
    }

    public void checkEveryItemLessOrEqualValue(String url, String path, String value) {
        checkStatusCodeInResponse(url, SUCCESS_CODE, POS_API_SCHEMA_EMPTY_JSON)
                .body(path,everyItem(lessThanOrEqualTo(parseFloat(value))));
    }

    public void checkEveryArrayHasItem(String path, String item, String posUrl, String schema) {
        checkStatusCodeInResponse(posUrl, SUCCESS_CODE, schema)
                .body(path, array((containsString(item))));
    }
}

Here we ask requestSpecification, which is a preset for requests. It will be used for all tests that will be inherited from this class. We pass in it the URI, the path, methods, for example, accept – it will check the data type for us. Here we see the use of the already familiar given, then and body.

Added a response time test (1500ms) to the base test, which does not fully replace load testing, but makes it possible to detect a performance problem at an earlier stage of testing.

Test.

  @DisplayName("Проверка соответствия первого офиса по городу \"Санкт-Петербург\"")
    @Positive
    @Test
    public void testSPB() {
        basePositiveHasItemsCheck("pid", "0819", getCityAndMetroAndLimitUrl("Санкт-Петербург", METRO_VALUE, LIMIT_1), POS_API_SCHEMA_JSON);
    }

And to check the answer, objects are described:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class FullPosOfficesPojo {
    private String pid;
    private String uuid;
    private String mnemonic;
    private String title;
    private String address;
    private List<MetroPojo> metro;
    private Boolean embossingEnabled;
    private List<String> prodType;
    private List<KindsPojo> kinds;
    private Boolean close;
    private String clientAvailability;
    private String addressOfficial;
    private List<String> schedule;
    private List<Object> temporarySchedule;
    private List<ConstraintPojo> constraints;
}

Further…

We write tests for positive and negative cases

basePositiveHasItemsCheck("pid", "0819", 
getCityAndMetroAndLimitUrl("Санкт-Петербург", METRO_VALUE, LIMIT_1), POS_API_SCHEMA_JSON);
}

Positive cases are marked with the positive tag.

The initial (raw) version of the tests
   /// Проверка параметров по умолчанию + город «Королёв"
    @Positive
    @Test
    public void testKorolev() {
        RestAssured.given(requestSpecification)
                .get("/offices?metroRadius=1000&city=Королёв").then()
                .assertThat()
                .statusCode(200)
                .body(matchesJsonSchemaInClasspath("schema.json"));
    }

    /// Проверка параметров по умолчанию + город "Санкт-Петербург"
    @Positive
    @Test
    public void testSPB() {
        RestAssured.given(requestSpecification)
                .get("/offices?metroRadius=3500&city=Санкт-Петербург").then()
                .assertThat()
                .statusCode(200)
                .body(matchesJsonSchemaInClasspath("schema.json"));
    }

Positive Checks:

    @Positive
    @Test
    public void testSPB() {
        RestAssured.given(requestSpecification)
                .get("/offices?metroRadius=3500&city=Санкт-Петербург").then()
                .assertThat()
                .statusCode(200)
                .body(matchesJsonSchemaInClasspath("schema.json"));
    }

Here we use the .assertThat function – it does the comparison.

         .get("/offices?metroRadius=3500&city=Санкт-Петербург").then()

And we see the builder pattern already familiar to us. Pay attention, here is the pattern:

RestAssured.given(requestSpecification)

And the autotest matches the builder pattern.

We add negative cases.

@Negative
@Test
public void testCityWithoutOffices() {
    ValidatableResponse response = posApiProvider.getOffices(requestSpecificationOffices,
    getCityAndMetroRadiusAndLimitTestData("Байконур", METRO_VALUE, LIMIT_1));
    checkResponseStatusCodeAndScheme(response, SUCCESS_CODE, POS_API_SCHEMA_EMPTY_JSON);
    checkResponseHasSizeInPath(response, "$", 0);
}

We mark them with the “negative” tag. Do not forget that you need to write tests for both valid and invalid data.

The initial (raw) version of the tests
/// Проверка запроса с пустым полем "город"
@Negative
@Test
public void testCityEmptyLine() {
    RestAssured.given(requestSpecification)
            .get("/offices?metroRadius=3500&city=").then()
            .assertThat()
            .statusCode(500);
}

/// Проверка запроса с пустым полем "радиус"
@Negative
@Test
public void testMetroRadiusEmptyLine() {
    RestAssured.given(requestSpecification)
            .get("/offices?metroRadius=&city=Москва").then()
            .assertThat()
            .statusCode(500);
}

Later on this project, we moved the parameters to endpoints and added checks for the rest of the request fields. In the future, we will add tests for checking values ​​according to the scheme. Why, using the same builder pattern, we will substitute the value into the schema.

If someone has suggestions on what else can be done, write in the comments, I will study with pleasure.

Of course, nowhere is without error.

My mistakes

Mistakes, first of all, which I made, these are such mistakes of a beginner – ignorance of what I am telling you now.

I started automating when the API was raw and changed a lot. I rewrote one JSON schema at least three times. About how many times I had to change the code, there is nothing to say.

And I also want to note that for each test, you should select the appropriate method, and not fence a bunch of code. So far, these are abstract words, but now I will explain to you with a concrete example.

We usually check if one object is equal to another using equals.

assertThat(retrievedEntity)
    .equals(expectedEntity,
        "number",
        "color",
        "date",
        "points",
        "price")
        …;

This check will not work if a new ID has been generated for a new object. Therefore, objects are separated by identifier fields. Fortunately, you can tell the method /assertThat ignore certain fields during the equality test, and we won’t have to load all this bunch of code here (and there can be even more fields).

And you can use…

assertThat(retrievedEntity)
    .isEqualToIgnoringGivenFields(expectedEntity, "id");

Is it really more convenient? We ignore the ID and the amount of code is reduced, it becomes much easier and more pleasant to maintain it.

Of course, at the initial stage of test automation, this is not so obvious due to ignorance of the syntax.

Advantages and disadvantages of API automation

There are many advantages.

  • Quick feedback.

  • Accurate thorough testing.

  • High test coverage.

  • Fast error detection.

  • Reusing tests.

  • Shorter delivery times.

  • Saving time and money.

But everything has, of course, its price. The price of implementing API autotests is the time spent on training. For example, I dived into REST API testing from scratch.

  • The training took me one month.

  • Designing, with questions from more experienced colleagues, took me one week.

  • Creating autotests at the moment is 2 weeks, but it continues now, and will continue, I think, for a long time.

  • Updating autotests will last until the very end, as long as this API is used.

I had a rough understanding of what REST is, but I had no understanding of how to automate checks. Of course, I cannot say that now I know everything about him. But at least I can write tests and somehow develop further.

Check list

Let’s sum up.

Preparation – what to pay attention to when we are preparing to implement autotests on the REST API.

  • First, evaluate the need to implement the API: do you need it at all or not.

  • Then learn the API under test, because you can’t test something you don’t understand.

  • Consider the specifics of your case.

  • Prepare the environment.

  • And you can proceed to the main actions.

Basic actions.

First, create a JSON-schema. Then write tests:

  • for a basic check

  • on extended positive cases;

  • and for negative tests, both with valid and invalid data, using the builder pattern, the REST-assured library and jUnit5 for this.

In tests:

  • Please note that the API looks at the correct stand and database.

  • Check if the HTTP status code is correct.

  • Check the response payload.

  • Check the response headers.

  • Check basic functionality.

Well, basically everything.

For help in preparing and correcting the code, I thank my QA colleagues. Was the article useful to you, did you learn something new for yourself?


Recommended articles:

Also subscribe to the Telegram channel Alfa Digital – there we post news, polls, videos from meetups, short excerpts from articles, sometimes we joke.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *