API Testing
User Journey Create Update and Get Employee Details

Visit here for a quick introduction to What is Declarative Testing And Zerocode (opens in a new tab)

Quick Overview

In Declarative Testing, the framework here does the job for us behind the scene i.e.

  • Making Http calls to the target end-point, with our request payload
  • Receiving the server response into the test case
  • Doing the result comparison of the actual vs expected response for our "assertions".
    • Here we can choose to skip the fields we do not need to assert
    • Or keep some fields as not null if server response is in-deterministic
    • We keep the payload as JSON structure as it is or we have the option to use the flat key-value assertions using the JSON Path
    • We have the option of comparing the results in LINIENT or STRICT mode (opens in a new tab)

It saves us from the major hassles of writing any code to do the above repetitive tasks.

Let's see how it is applied to a user journey.

USER JOURNEY - Acceptance Criteria(ACs)

  • AC1
GIVEN- The Create API POST:"/api/v1/employees"
WHEN I invoke the POST operation with a "employee" payload
and content-type as "application/json"
THEN I will create a new employee
AND validate the 201(created) status
and verify the response has only the employeeId(strict matching).
  • AC2
GIVEN- The Get API GET:"/api/v1/employees/{employeeId}"
WHEN I invoke the GET operation with max 5 retries with 1sec gap
THEN I will fetch the employee details
AND validate the status as 200(OK) along with
AND response has the expected payload with the required fields(lenient matching).

To write a test-case for the above CRUD operation scenario is quite easy using Zerocode (opens in a new tab), just our IDE's JSON editor is easy enough to hook these rwo steps/ACs. For instance, POST and GET step would look like below(simple and clean).

Hosts and ports are externalized to a properties file for env switching

And at the same time we don't have to search for or think hard of any syntaxes to do the job. That means, we are ready with a BDD test scenario quickly with these simple JSON steps(see below).

The advantage here is the tests are instantly readable to any stakeholder

View the JSON or YAML based test-scenario below.

Using JSON

JSON (Click to expand)

{
    "scenarioName": "Validate the POST and GET employee API",
    "steps": [
        {
            "name": "create_emp",
            "url": "/api/v1/employees",
            "method": "POST",
            "request": {
                "headers": {
                    "Content-Type": "application/json"
                },
                "body": {
                    "id": "DEP-CROY-9001",
                    "name": "Oliver",
                    "postcode": "EC2 9XY"
                }
            },
            "retry": {
                "max": 3, //<--- configure to your "retry" requirement or skip it
                "delay": 1000
             },
            "verify": {
                "status": 201,
                "body": {
                    "id": "DEP-CROY-9001"
                }
            },
            "verifyMode": "STRICT"
        },
        {
            "name": "get_emp",
            "url": "/api/v1/employees/${$.create_emp.response.body.id}",
            "method": "GET",
            "retry": {
                "max": 5,
                "delay": 1000
            },
            "request": {
                "headers": {
                    "Content-Type": "application/json"
                }
            },
            "verify": {
                "status": 200,
                "body": {
                    "id": "${$.create_emp.response.body.id}",
                    "deptCode": "DEP.ASY"
                }
            },
            "verifyMode": "LENIENT"
        }
    ]
}

Using YAML

YAML (Click to expand)

Or you can use YAML DSL

  • YAML DSL
---
scenarioName: Validate the POST and GET employee API
steps:
  - name: create_emp
    url: "/api/v1/employees"
    method: POST
    retry:
      max: 3
      delay: 1000
    request:
      headers:
        Content-Type: application/json
      body:
        id: DEP-CROY-9001
        name: Oliver
        postcode: EC2 9XY
    verify:
      status: 201
      body:
        id: DEP-CROY-9001
    verifyMode: STRICT
  - name: get_emp
    url: "/api/v1/employees/${$.create_emp.response.body.id}"
    method: GET
    retry:
      max: 5
      delay: 1000
    request:
      headers:
        Content-Type: application/json
    verify:
      status: 200
      body:
        id: "${$.create_emp.response.body.id}"
        deptCode: DEP.ASY
    verifyMode: LENIENT

That's it, done. We are ready to run the above scenario.

The hosts and ports in the url fields are externalized as usual(explained below).


Then we stick the above json/yml file to a JUnit runner and run. We can point to any host and port in the Runner. See the sample below.

@TargetEnv("application_host.properties")
@RunWith(ZeroCodeUnitRunner.class)
public class JustHelloWorldTest {
 
    @Test
    @Scenario("helloworld/user_crud_journey_test.json")
    public void testGet() throws Exception {
       // No code is needed here.
    }
}

the application_host.properties looks as below:

web.application.endpoint.host=https://hbc.banking.co.local.uk
web.application.endpoint.port=443
web.application.endpoint.context=
                             Done. Happy days!

If You Have Time To Read

If you have little more time to read below, see what all hassles we escaped and how much time we saved !

  • No need to write pojos and builder for the domain objects(e.g. Employee here)
  • No need of any serialization/deserialization
  • No need of http client calls and read the response
  • No need to assertThat(expected, is(actual)) etc multiple times
  • No need of any feature files and syntax searchings
  • No need of English statements and grammars

What We Did Not Have to Do(luckily)

JOURNEY1 :

  • Step 1
~~Employee emp =~~
    ~~EmployeeBuilder.aNewInstance();~~
    ~~.name("Larry P")~~
    ~~.job("Full Time")~~
    ~~.build()~~

Make the POST call

~~ObjectMapper objectMapper = ObjectMapperProvider.getInjectedObjectMapper();~~

~~HttpResponse<> postResponse =~~

~~aHttpClient.post("http://host:port/api/v1/persons")~~

  ~~.header("accept", "application/json")~~

  ~~.body(objectMapper.writeValueAsString(emp))~~

  ~~.execute();~~

Assert the response

~~assertThat(postResponse.getStatusCode(), is(201))~~

~~assertThat(postResponse.getEntity().getId(), is(1001))~~
  • Step 2

Create an employee with updated payload

~~Employee empForUpdate = EmployeeBuilder.aNewInstance()~~
    ~~.name("Larry Page")~~
    ~~.job("Co-Founder")~~
    ~~.build();~~

Make a PUT call

~~HttpResponse<Employee> putResponse =~~

~~aHttpClient.put("http://host:port/api/v1/persons/" + postResponse.getEntity().getId())~~

  ~~.header("accept", "application/json")~~

  ~~.body(objectMapper.writeValueAsString(empForUpdate))~~

  ~~.execute();~~

~~Employee empUpdated = response.getUser();~~

Assert the response

~~assertThat(putResponse.getStatusCode(), is(200))~~

~~assertThat(empUpdated.getName(), is(empForUpdate.getName()))~~

~~assertThat(empUpdated.getJob(), is(empForUpdate.getJob()))~~
  • Step 3
> Make the GET call

~~HttpResponse<Employee> response =~~

~~aHttpClient.get("http://host:port/api/v1/persons/" + postResponse.getEntity().getId())~~

  ~~.header("accept", "application/json")~~

  ~~.execute();~~

~~Employee empFetched = response.getEmployee();~~

Assert the response

~~assertThat(response.getStatusCode(), is(200))~~

~~assertThat(empFetched.getName(), is(empForUpdate.getName()))~~

~~assertThat(empFetched.getJob(), is(empForUpdate.getJob()))~~

Also, we escaped the hard way of doing things with special attention to English statements and grammars. See below:


This approach might take different shapes and forms for developers/test-engineers who need to spend lot of time agreeing on the semantics rather than spending time in creating actual executable test-scenarios.

e.g.

~~GIVEN- the REST api POST end point,~~ <br/>
~~WHEN- I invoke the API with a payload,~~ <br/>
~~THEN- I will receive 201(Created) status with a newly created ID and assert the response~~ <br/>

or

~~GIVEN- the REST url and the method POST,~~ <br/>
~~WHEN- I invoke the API with a body,~~ <br/>
~~THEN- I will receive 201(Created) status with newly created ID~~ <br/>
~~AND assert the response~~ <br/>

or

~~GIVEN- the REST url /api/v1/persons/ ~~ <br/>
~~AND the http method POST ~~ <br/>
~~WHEN- I invoke the API using a HTTP client and send the body,~~ <br/>
~~THEN- I will receive 200(OK) status with body~~ <br/>
~~AND assert the response~~ <br/>

and so on...

Note- Too much is going on the above around an user journey, in terms of writing correct sentences or nearly correct sentences/grammars, too many assertThats to come up with a test scenario.

And imagine the maintenance overhead and difficulty in tracing the steps, when we have more number of steps in an user journey !


:::Note:::

It might(arguably) make sense for the BAs(Business Analysts) or non-technology folks while creating the stories and defining the entry and exit criteria of the tickets for a business scenario or User-Journey. But the techincal folks simply picking these statements and trying hard syntactically to fit these into the executable scenarios seems like bit too much of a wastage of time and maintenance overhead.

But we should choose the tools, technologies and solutions which best fits to our project and situation and really helps us solving the testing challenges.

See This in Action(HelloWorld):

The simplified HelloWorld projects are in GitHub repo to clone and run locally