5 Steps to Create REST API Component test

Author: Geethalaksmi Ramachandran


API tests provide quick feedback, resilience against brittleness in tests compared to UI end-end tests, and provide consistent results. The business logic implemented in the server-side as an API does not need to be tested from the UI/presentation layer. Often times, this becomes the automation test strategy since most of the teams use UI test frameworks like Selenium/Protractor. UI end-end tests are flaky since they are dependent on the browser behavior, UI element states, network and database response time. Flaky tests require debugging and so are expensive to maintain. The business logic implemented at the API-layer can be tested directly instead of depending on the UI layer to execute the tests and will align with the pyramid test approach.

In 5 easy steps, though, you can write API tests using rest-assured, Gson, Guava.

This post assumes that the test is written in Junit/Java and the REST endpoint accepts JSON format. It provides examples to test GET API from a smoke test and from a functional integration perspective by validating the results with database, and to test POST API.

Pre-requisites

  • Maven project

1. Create property file

The property file is to store common properties instead of directly coding them in the test code. This helps segregate any environment-dependencies from the test. A test can be executed on a local service. It can also be run on a deployed service connected to real back-end by pointing the test to execute against different properties file.

    
database.driver=org.hsqldb.jdbcDriver
database.url=jdbc:hsqldb:file:/Projects/Blog/RestTestExample/src/test/resources/db/file.db
database.username=sa
database.password=
 
#REST API URL
BASE_URL=http://localhost:3000/

2. Add Maven Dependencies:

These include:

  • log4j - Logging framework
  • junit - Test runner
  • RestAssured - To send request and receive response from the rest service
  • gson - Google's json framework to de/serialize
  • guava - Google's extended collections for MapUtilities
  • springframework - jdbctemplate to query and get results from DB
  • hsqldb - In this example, we are connecting to local hsql database. Change this as needed for postgres/Oracle/etc
    

    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
   </dependency>
   <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
   </dependency>
   <dependency>
      <groupId>com.jayway.restassured</groupId>
      <artifactId>rest-assured</artifactId>
      <version>2.4.0</version>
      <scope>test</scope>
   </dependency>
   <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.5</version>
      <scope>test</scope>
   </dependency>
   <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>4.0.3.RELEASE</version>
   </dependency>
   <dependency>
      <groupId>org.hsqldb</groupId>
      <artifactId>hsqldb</artifactId>
      <version>2.3.2</version>
      <scope>test</scope>
   </dependency>
   <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>19.0</version>
      <scope>test</scope>
   </dependency>
</dependencies>

3. Create a test class

Below are the properties that are defined in the class.

  • The test uses log4j for logging, hence a need for a logger instance.
  • Since we created a property file in Step1, we're creating a properties object to load the properties.
  • In this case, we're using EmbeddedDatabase for connecting to HSQL database. If you are connecting to Oracle/postgres - you can define through Datasource.
  • We're using JdbcTemplate to execute queries and retrieve results.
  • We're using Gson to serialize objects into json. See an example of this in Step 5.c
    
public class ExampleTestDemo {
final static Logger logger = Logger.getLogger(ExampleTestDemo.class);
private static Properties props;
private static String PROPERTIES_FILE = "src/test/resources/app-dev.properties";
private static String DB_DDL_LOCATION="file:///C:/Projects/Blog/RestTestExample/src/test/resources/db/my-schema.sql";
private static String DB_SCRIPT_LOCATION="file:///C:/Projects/Blog/RestTestExample/src/test/resources/db/my-test-data.sql";
private static EmbeddedDatabase db;
JdbcTemplate template = new JdbcTemplate(db);
Gson gson = new GsonBuilder().serializeNulls().create();
}RequestSpecification
 

For connecting to the REST APIs using Restassured, we need to define a RequestSpecification. Below is a generic requestspec that sets the content-type and accept-type to JSON format. This can be formatted differently depending on the format supported by the API. This is being setup in the class because different test methods can share this. If you need to create different requestspecification, it can be created in each test method as well according to the needs of the test.

ResponseSpecification ResponseSpecification is used to validate the API response whether it is conforming to a particular expectation. In this case, we are expecting that all response should be successful with 200 status code.


//Common Request Spec and Response Spec property across all tests
RequestSpecBuilder reqSpecBuilderGET = new RequestSpecBuilder().log(LogDetail.ALL)
    .setContentType(ContentType.JSON)
    .setAccept(ContentType.JSON);
 
ResponseSpecification
commonResponseSpec =
new ResponseSpecBuilder().expectStatusCode(200).expectStatusLine("HTTP/1.1 200 OK").build();

4. Create BeforeClass and AfterClass methods

Each test class needs to be self-sufficient in setting up and tearing down the resources needed for the tests. Simple housekeeping would go a long way! Resources needed in this example are properties file and HSQL DB. Assumption here is that the app is already running in localhost and listening on port 3000 (as setup in the property file in Step 1). This can also access a real-live endpoint that is deployed in a physical server. In that case, the test should be accessing the back-end DB by defining the Datasource.

    
@BeforeClass
public  static void setup() {
    try {
        props = new Properties();
        FileInputStream fin = new FileInputStream(PROPERTIES_FILE);
        props.load(fin);
        fin.close();
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        db = builder.setType(EmbeddedDatabaseType.HSQL).addScript(DB_DDL_LOCATION).addScript(DB_SCRIPT_LOCATION).build();
    } catch (IOException e) {
        logger.info ("Error in reading the properties file located at : "+PROPERTIES_FILE);
        e.printStackTrace();
    }
}
@AfterClass
public static void teardown()
{
    db.shutdown();
}

5. Create API test

We will see examples of how to test:

  • GET API in a smoke test
  • GET API in an Integration test that connects and validates from database
  • POST API

5.a. Test GET API in a smoke test

This is a simple test that can be part of the Smoke TestSuite. It accesses the API and make sure that the API returns expected status code and the response contains a valid string. It does not validate the functionality of the API in its entirety, hence it does not validate the complete response from the service. It only validates that the API is up and running. The smoke test is helpful for quick verification and in post-deployment verification tests.

    
@Category(SmokeIntegrationTest.class)
@Test
public void testGETSmokeTest()
{
 String GETURL = props.getProperty("BASE_URL")+"posts/1";
 Response response = makeGETRequest(GETURL);
 logger.info (response.prettyPrint());
 response.then().spec(commonResponseSpec);
 response.then().assertThat().body(containsString("FINRA"));
}

Below is the method that connects to the GET API using rest-assured. This uses the request specification that was created as class property in Step 3.

    
public Response makeGETRequest (String URL)
{
    Response
        response =
        RestAssured.given().spec(reqSpecBuilderGET.build()).redirects().follow(true).when()
            .get(URL).then().extract()
            .response();
 return response;
}
 

5.b. Test GET API in an integration test that connects to and validates from database

Below is an example of an integration test that validates the functionality of the GET API with database. Most often the GET API would return data from database after filtering and/or transformation.

The API result is stored in the response object which is then stored in List of Maps datastructure for easy comparison. This is the actual result. The test gets the expected result by querying the database and stores in the List of Maps datastructure. All the test should do now is validate the actual and expected results.When saving the actual and expected result in the data structure, the format is converted to a string to avoid data formatting anomalies in the comparison between actual and expected result. The query uses column-aliases to return the same column names as returned by the API.

    
@Category(FunctionalIntegrationTest.class)
@Test
public void testGETIntegrationTestWithDB()
{
 String GETURL = props.getProperty("BASE_URL")+ "posts";
 Response response = makeGETRequest(GETURL);
 logger.info ("\n Response : "+ response.prettyPrint());
 response.then().spec(commonResponseSpec);
 String query = "select id as \"id\",title as \"title\", body as \"author\" from POST order by id";
 List> responseMap = response.path("");
 List> ActualResponseMap =  ObjecttoStringConverter(responseMap);
 List> ExpectedResponseMap =  ObjecttoStringConverter(template.queryForList(query));
 logger.info ("\n Actual : \n "+ ActualResponseMap);
 logger.info ("\n Expected : \n "+ExpectedResponseMap);
 ListMapCompareValidator(ExpectedResponseMap,ActualResponseMap);
}
 

Now, we have two sets to compare - The actual result from the API and the expected result from the DB. In this example, we are using Guava's Maps.Difference() to compare the actual and expected List of Maps. Maps.Difference() returns three maps- entries in Left, entries in Right and entries differing (similar to a Venn diagram). The test should expect that both the actual and expected are the same and contain no differences. Hence it asserts that entries in left should be empty, entries in right should be empty and entries in differing should be empty which means that both the maps have all common entries only.

This could have been achieved with a simple equals method as well. That implementation, however, wouldn't have provided a meaningful log as to why the test failed unless it is debugged. In this implementation, the test logs on why the comparison failed, by logging the entries that are differing between the two Maps.

    
public void ListMapCompareValidator(List> ExpectedResponseMap,List> ActualResponseMap)
{
 
    logger.debug (ActualResponseMap);
    logger.debug (ExpectedResponseMap);
 
    int expectedSize=ExpectedResponseMap.size();
    int actualSize=ActualResponseMap.size();
    Assert.assertEquals("Expected Size NOT Equal to Actual Size. ExpectedSize= " + expectedSize+". Actual Size = "+actualSize,expectedSize,actualSize);
    for (int i=(expectedSize > actualSize ? expectedSize : actualSize);i>0;i--)
    {
        Iterator> expectedItr = ExpectedResponseMap.iterator();
        Iterator> actualItr = ActualResponseMap.iterator();
        Map  expectedMap=expectedItr.next();
        Map  actualMap=actualItr.next();
        MapDifference mapDiff = Maps.difference(expectedMap, actualMap);
        expectedItr.remove();
        actualItr.remove();
        if (mapDiff.entriesDiffering().size()!=0)
        {
            logger.info("Expected Contents NOT Equal to Actual Contents. Differences = " + mapDiff.entriesDiffering());
        }
        if (mapDiff.entriesOnlyOnLeft().size()!=0)
        {
            logger.info("Expected Contents NOT Equal to Actual Contents. Entries Only on Left = " + mapDiff.entriesOnlyOnLeft());
        }
        if (mapDiff.entriesOnlyOnRight().size()!=0)
        {
            logger.info("Expected Contents NOT Equal to Actual Contents. Entries Only on Right = " + mapDiff.entriesOnlyOnRight());
        }
        Assert.assertEquals("Expected Contents NOT Equal to Actual Contents. Differences = " + mapDiff.entriesDiffering(),mapDiff.entriesDiffering().size(), 0);
        Assert.assertEquals("Expected Contents NOT Equal to Actual Contents. Entries Only on Left = " + mapDiff.entriesOnlyOnLeft(),mapDiff.entriesOnlyOnLeft().size(), 0);
        Assert.assertEquals("Expected Contents NOT Equal to Actual Contents. Entries Only on Right = " + mapDiff.entriesOnlyOnRight(),mapDiff.entriesOnlyOnRight().size(), 0);
    }
}

We use helper method - ObjectToStringConverter() to convert object type to string to avoid any formatting issues while comparison.

    
public List> ObjecttoStringConverter(List> responseMap)
{
    List> responseStringMap = new ArrayList>();
    for (Map map : responseMap) {
        Map tempMap = new LinkedHashMap();
        for (Map.Entry entry : map.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue().toString();
            tempMap.put(key, value);
        }
        responseStringMap.add(tempMap);
    }
    return responseStringMap;
}
 

5.c. Test POST API

Most POST APIs require a request body. The request body can be in XML/JSON. In this example, we are assuming the request body is in JSON. The JSON request body can be constructed as a string. Most projects use object model and hence constructing the JSON from the object is quicker and easier. The code below shows how we use Gson to serialize an object into JSON and use it to access the POST API.

        
@Category(FunctionalIntegrationTest.class)
@Test
public void testPOSTAPIUsingObject ()
{
 
    post po=new post("3","testPost","FINRA");
 String POSTURL = props.getProperty("BASE_URL")+ "posts";
 String reqBody = gson.toJson(po);
 Response response = makePOSTRequest(POSTURL,reqBody);
 logger.info ("\n Response : "+ response.prettyPrint());
 Assert.assertTrue(getJsonObject(response).get("id").getAsInt()==3);
 
}

This code connects to a POST API with a request body (reqbody).

        
public Response makePOSTRequest(String URL, String reqBody)
{
    RequestSpecBuilder reqSpecBuilder = new RequestSpecBuilder().log(LogDetail.ALL)
        .setBody(reqBody)
        .setContentType(ContentType.TEXT)
        .setAccept(ContentType.JSON);
 
 Response
        response =
        RestAssured.given().spec(reqSpecBuilder.build()).redirects().follow(true).when()
            .post(URL).then().extract()
            .response();
 return response;
}

Following these steps, we are able to write tests for GET and POST API. Similar approach can be used for testing PUT and DELETE too. The example shows different test strategies (Eg. smoke test) where the tests can assert a quick functional test and where the functionality is tested along with the integration with the database. You can pick and choose the approach that works best for your use case. This shows how to validate business logic implemented in the REST API as a whole component. They do not replace unit tests. Unit tests still need to be written to cover each control flow/method to have test coverage for all cases handled in the code.