Rest API Testing: How to test Rest APIs properly!

restapi-testing-1

Rest API testing is one of the most important tasks when developing reliable and secure Rest APIs. Here you will find a complete overview of everything you should consider when testing your Rest API.

There are countless ways to test a Rest API. The procedure depends, for example, on the application, the programming language and the development team. However, there are a few basic things you should always bear in mind. And that’s exactly what this blog post is about.

Why you should test your Rest API

An API is like a contract between a client and a server or between two applications. Before testing the implementation, it must be ensured that the contract is correct by checking the specification (e.g. Swagger or OpenAPI). For Rest APIs, it is important that all HTTP REST semantics and principles are adhered to. This is all the more important for publicly accessible APIs so that all functionalities can continue to be guaranteed after updates.

Security checks are an essential part of the testing process. By testing, you can uncover and close potential security gaps to protect your application from attacks. Another important aspect is optimising performance. Through testing, you can measure the response times of your API and ensure that it remains fast and reliable even under high load. In this way, bottlenecks and performance problems can be recognised and rectified at an early stage.

Last but not least, testing enables the integration of your API into a Continuous Integration and Deployment (CI/CD) pipeline, which automatically checks changes to the code and ensures continuous quality assurance.

Selection of the optimal test cases

Before we can start writing the tests, I would like to briefly explain how to select the right test cases. This is actually the most important task. Otherwise we can write a hundred tests, none of which add any value. We need to think particularly carefully about edge cases. Edge cases are test inputs that are most likely to trigger a faulty output or even a programme error.

Note: There are different types of tests, e.g. integration tests, unit tests or functional tests. Various tests are shown in this blog post, but I will not go into the differentiation of the test types in detail.

To make the selection a little clearer, I have come up with a case study. These are our requirements:

  • We want to create a function to register a user (/sign-up route)
  • Username, password and password repetition should be queried
  • The user name must not yet be assigned
  • The user should receive an e-mail to complete the registration (double opt-in)

With a little logical thought, the following test cases come to mind first:

  • Have all parameters (user name, password, password repetition) been transferred?
  • Does the user name already exist in the database?
  • Has the confirmation e-mail been sent?

That is all correct, but we have forgotten a few more. And these are the so-called marginal cases:

  • Is the HTTP request method correct?
  • Have all required parameters been transferred in the correct format?
  • Do the password and password repetition match?
  • Is the password length sufficient?
  • Could the database query be executed without errors?
  • Are the correct HTTP status codes returned in the event of an error?
  • Is the user name not too long for the database field (e.g. if VARCHAR is limited to 255 characters)?
  • Has a valid token been created for the double opt-in and successfully saved in the database?
  • Are the entries XSS and SQL injection-proof?

The aim is to look at the enquiry from a different angle in order to cover as many edge cases as possible. I hope that I can sensitise you to this with this example.

Take your time to select the test cases. This is more important than writing the tests. Once you have done this correctly, you will save yourself a lot of time in the future.

Rest API Testing Tools

There are numerous tools for testing Rest APIs. There are tools with which we can execute HTTP requests directly or tools with which we can write unit tests and thus test our Rest API. These are my two favourite ways:

  • Postman: Postman is a widely used tool that provides an intuitive user interface for testing and debugging Rest APIs. With Postman, you can quickly and easily create, test and save API requests. It is easy to use and working with multiple developers on the same resources works great.
  • Unit Testing: Yes, Rest APIs can also be tested using unit tests. This involves executing the HTTP requests and validating the result. Although this method initially requires more effort, it offers the advantage of complete integration into your development process and enables continuous testing. Each programming language has its own tools. I mainly develop with JavaScript, so I can show you a practical example here. I use Mocha and Chai as testing frameworks. For each test, one or more requests are sent to the corresponding Rest API endpoints and the result is validated.
const chai = require('chai');
const chaiHttp = require('chai-http');
const server = require('../../index');
const should = chai.should();
require('dotenv').config();

chai.use(chaiHttp);

describe('## Entries - Test entry functionalities', () => {
  let token;
  let listId;

  // authenticate user before running tests
  before((done) => {
    // ...
  });

  it('should be able to create a new entry', (done) => {
    const entry = {
      name: 'Test entry',
    };

    chai
      .request(server)
      .post(`/api/${process.env.API_VERSION}/list/${listId}/entries`)
      .set('Authorization', `Bearer ${token}`)
      .send(entry)
      .end((err, res) => {
        res.should.have.status(201);
        res.should.be.json;
        res.body.should.have.property('id');
        res.body.should.have.property('listID');
        res.body.should.have.property('name');
        res.body.should.have.property('checked');

        const entry = res.body;

        // checked should be false by default
        entry.checked.should.equal(false);

        done();
      });
  });

  it("shouldn't be able to create a new entry if the list does not exist", (done) => {
    const entry = {
      name: 'Test entry',
    };
    const unrealisticListId = 9999999;

    chai
      .request(server)
      .post(`/api/${process.env.API_VERSION}/list/${unrealisticListId}/entries`)
      .set('Authorization', `Bearer ${token}`)
      .send(entry)
      .end((err, res) => {
        res.should.have.status(404);
        done();
      });
  });

  it("shouldn't be able to create a new entry if the name is not provided", (done) => {
    const entry = {};

    chai
      .request(server)
      .post(`/api/${process.env.API_VERSION}/list/${listId}/entries`)
      .set('Authorization', `Bearer ${token}`)
      .send(entry)
      .end((err, res) => {
        res.should.have.status(400);
        res.text.should.eql('Missing name property');

        done();
      });
  });

  it('should be able to get all entries for a list', (done) => {
    chai
      .request(server)
      .get(`/api/${process.env.API_VERSION}/list/${listId}/entries`)
      .set('Authorization', `Bearer ${token}`)
      .end((err, res) => {
        res.should.have.status(200);
        res.should.be.json;
        res.body.should.be.a('array');

        done();
      });
  });

  it("shouldn't be able to get all entries for a list if the list does not exist", (done) => {
    const unrealisticListId = 9999999;

    chai
      .request(server)
      .get(`/api/${process.env.API_VERSION}/list/${unrealisticListId}/entries`)
      .set('Authorization', `Bearer ${token}`)
      .end((err, res) => {
        res.should.have.status(404);
        res.text.should.eql('List not found');
        done();
      });
  });

  it('should be able to update the properties of an entry', (done) => {
    let entry = {
      name: 'Default name',
      checked: false,
    };

    // create a new entry
    chai
      .request(server)
      .post(`/api/${process.env.API_VERSION}/list/${listId}/entries`)
      .set('Authorization', `Bearer ${token}`)
      .send(entry)
      .end((err, res) => {
        res.should.have.status(201);

        const createdEntryId = res.body.id;

        // update the entry
        let changedName = 'Changed entry name';
        entry.name = changedName;
        entry.checked = true;
        entry.priority = 99;

        // send the patch request
        chai
          .request(server)
          .patch(
            `/api/${process.env.API_VERSION}/list/entries/${createdEntryId}`
          )
          .set('Authorization', `Bearer ${token}`)
          .send(entry)
          .end((err, res) => {
            res.should.have.status(200);
            res.should.be.json;

            // check if the name has changed
            res.body.name.should.equal(changedName);

            // check if the checked state has changed
            res.body.checked.should.equal(true);

            // check if the priority has changed
            res.body.priority.should.equal(99);

            // delete the entry
            chai
              .request(server)
              .delete(
                `/api/${process.env.API_VERSION}/list/entries/${createdEntryId}`
              )
              .set('Authorization', `Bearer ${token}`)
              .end((err, res) => {
                res.should.have.status(200);
                done();
              });
          });
      });
  });
[...]

There are similar tools in other programming languages. Here are a few more common Rest API testing tools:

  • JUnit: A popular framework for testing Java applications. It is ideal for creating unit tests for rest APIs in Java.
  • RestAssured: A DSL-based Java tool developed specifically for testing Rest APIs. It provides a simple and expressive syntax for validating API requests and responses.
  • PyTest: A well-known Python framework for writing unit tests in Python.

Rest API Tests in Postman

Postman is one of my favourite tools for testing Rest APIs. I would therefore like to show you in more detail how you can create tests.

Organise requests in Postman

If you have installed Postman, this interface awaits you. You should create a separate collection for each project/rest API. This is like a folder in which you can organise your individual requests. In addition, you can then execute the tests for all the requests it contains with a single click.

Postman - Create collection
Postman – Create collection

We can then create any number of requests per collection. I have highlighted the most important settings in red.

Structure of an HTTP request
Structure of an HTTP request

Firstly, we should give our request a name so that it can be recognised quickly (‘Login’ in this case). This also appears on the left in the collection menu.

Next, we can set the corresponding HTTP request method for each request and specify the exact URL of the Rest API route to be checked.

Tip: You should create recurring data such as the base URL (http://localhost:3000) as variables.

On the far right you will find the ‘Send’ button to send an enquiry directly.

Transfer body data

In many cases, we want to send data to the Rest API. We can either do this via the ‘Params’ tab. These are then transmitted to the server according to the HTTP request method or we use the ‘Body’ tab to transmit data in other data formats.

JSON is often used on the web as it is very flexible and can be used to transfer complex data structures. It is important that you select ‘raw’ above the text field and then select the ‘JSON’ format to the right of it. Otherwise the data may not be transferred correctly.

Pass body parameter in Postman
Pass body parameter in Postman

The data can now be transferred to the large text field in JSON format.

This can look like this, for example. Postman shows you errors in the JSON structure directly. However, you can test it more precisely with this JSON validator.

{
"username": "webdeasy.de",
"password": "f00b4r",
"password_repeat": "foob4r"
}

Whether you use URL parameters or body data depends entirely on the implementation of your Rest API. This is how you access it in Node.js:

router.post('/sign-up', (req, res, next) => {
	// body data
	console.log(req.body.YOURDATA);
	
	// url parameters
	console.log(req.params.YOURPARAM)
});

Insert Authorisation Keys

If you have implemented a login with Bearer Token, you can transfer this in the ‘Authorisation’ tab. To do this, select ‘Bearer Token’ and enter it on the right under ‘Token’.

Authorisation - Transfer Bearer Token
Authorisation – Transfer Bearer Token

For other authentication methods, you can select the appropriate one under ‘Type’.

Tip: You can find your last queries under the ‘History’ menu item.

Programming test cases in Postman

We have entered and clearly structured the individual routes. You can now execute and test them using the ‘Send’ button. However, you always have to check all results manually. It is easier if a script takes over the task for us. We can do this under the ‘Tests’ tab.

Create test cases with Postman
Create test cases with Postman

Here you have the opportunity to programme your test cases, which is easier than it sounds at first.

We can use the following code to query the correct status code:

pm.test("Status test", function () {
    pm.response.to.have.status(200);
});

It can be that simple. What you will also often need is to query a response header variable. Here I check whether the response is an image of the type ‘jpeg’:

pm.test("Content-Type is image/jpeg", function () {
   pm.response.to.have.header("Content-Type");
   pm.response.to.be.header("Content-Type", "image/jpeg");
});

Tip: On the right-hand side under ‘Snippets’ you can click to insert ready-made tests.

More detailed documentation on creating test cases is available directly at learning.postman.com.

Execute tests automatically

We have created our test cases manually. However, to save us some time, we can run all the tests together in a collection. To do this, select the collection and click on ‘Run’. You can then set the number of iterations (executions) and other points.

Execute collection test
Execute collection test

You will then receive a detailed test log and can rectify errors in the code directly and run the test again.

Test protocol of the two tests
Test protocol of the two tests

Automatic Rest API tests in a CI/CD pipeline

It is a good and popular practice to run tests directly from a CI/CD pipeline. As a result, many errors are seen directly when the code is checked into the repo and can be rectified. In the best case, before everything is deployed to production.

The following code is an excerpt from a GitHub Actions workflow file, in which tests of a Node.js backend are executed.

name: Execute Unit Tests

on:
  push:
    branches: [ "develop" ]

jobs:
  test:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [20.x]

    steps:
    - uses: actions/checkout@v3
    
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        
    - name: Install dependencies and run tests
      run: |
        npm install
        npm run test
      env:
        CI: true # set environment variable to run tests in CI mode

Cool, right? I’ve created a detailed blog post explaining how to create CI/CD pipelines with GitHub Actions.

Rest API Testing: Summary

Testing Rest APIs is an important step in ensuring the quality and performance of the application. By carefully selecting test cases and using different test methods, potential problems can be recognised and rectified at an early stage. Integration into your CI/CD pipeline enables continuous review, while adherence to the API specification ensures contract compliance.

Related Posts
Join the Conversation

8 Comments

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

bold italic underline strikeThrough
insertOrderedList insertUnorderedList outdent indent
removeFormat
createLink unlink
code

This can also interest you