Building a Strong Foundational Framework for API Testing: Best Practices and Key Insights

Building a Strong Foundational Framework for API Testing: Best Practices and Key Insights

In the fast-evolving world of software development, API testing plays a crucial role in ensuring that applications run smoothly and efficiently. Cypress, a popular end-to-end testing framework, is increasingly adopted for API testing due to its simplicity, speed, and rich ecosystem. This blog will explore the basics of setting up an API test framework with Cypress, delve into best practices, and provide a sample code structure to kickstart your journey.



Introduction: API testing involves validating the functionality, performance, and reliability of APIs that serve as the backbone of modern applications. Cypress is best known for UI testing but shines equally in API testing, thanks to its ability to run tests quickly and its robust JavaScript-based tooling.

A well-structured API test framework in Cypress ensures:

  1. Scalable test development

  2. Improved debugging capabilities

  3. Integration with CI/CD pipelines for continuous feedback

Let’s dive into building a strong foundation for your Cypress API testing.


API Test framework setup basics: In this section, we will discuss setting up the test framework and some basics to understand the process properly. Additionally, there will be a step-by-step guide with some sample codes to make things easier for all!

  1. Install Cypress
    Start by installing Cypress into your project:

     npm install cypress --save-dev
    
  2. Organize Project Structure
    Create a clear directory structure for your API tests. For example:

/cypress
  /integration
    /api
      - sampleTests.cy.js
  /support
    - commands.js
  /fixtures
    - sampleData.json
    - ids.json
    - userdata.json
  1. Configure Environment Variables
    Add sensitive information, like base URLs and API keys, in the cypress.config.js or .env file:
env: {
  baseUrl: 'https://api.example.com',
  apiKey: 'your-api-key'
}
  1. Organize API Endpoints

    Keeping all API endpoints in a centralized location makes tests more maintainable and reduces the chances of hardcoding URLs in individual test files.

    File: cypress/support/apiEndpoints.json

export const API_ENDPOINTS = {
    LOGIN: "/api/users/login",
    USERS: "/api/users",
    USER: "/api/users/{id}",
    LOGOUT: "/api/users/logout",
    DETAILS: "/api/dash/clients",
};
  1. Usage of base URL and API endpoints in tests

    In your test files, import API_ENDPOINTS to use the predefined routes.

    File: cypress/integration/api/userTests.cy.js
import { API_ENDPOINTS } from '../../support/apiEndpoints';

describe('User API Tests', () => {
  const baseUrl = Cypress.env('baseUrl'); // Mount the base URL in a variable

  it('Should create a new user', () => {
    cy.fixture('userdata').then((requestBody) => {
      cy.request({
        method: 'POST',
        url: `${baseUrl}/${API_ENDPOINTS.USERS}`,
        body: requestBody,
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${Cypress.env('apiKey')}`
        }
      }).then((response) => {
        expect(response.status).to.eq(201);
      });
    });
  });

  it('Verify if a user can login', () => {
    cy.fixture('userdata').then((requestBody) => {
      cy.request({
        method: 'POST',
        url: `${baseUrl}/${API_ENDPOINTS.LOGIN}`,
        body: requestBody,
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${Cypress.env('apiKey')}`
        }
      }).then((response) => {
        expect(response.status).to.eq(200);
      });
    });
  });
});

Explanations:

  • In the code snippet, only two API endpoints are used. i.e. USERS and LOGIN. The rest of the mentioned variables in API_ENDPOINTS can be used similarly.

  • Also, the baseUrl is used to mount the cypress env variable and is used later in all tests.

  1. Parameterize Endpoints

When API endpoints include dynamic segments like id, use fixture data to fetch id and replace it with the variable:

import { API_ENDPOINTS } from '../../support/apiEndpoints';

describe('User API Tests', () => {

  it('Should create a new user', () => {

    cy.fixture('ids').then((ids) => {
      cy.request({
        method: 'GET',
        url: `${Cypress.env('baseUrl')}/${API_ENDPOINTS.USER}/${ids.client_id}`,
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${Cypress.env('apiKey')}`
        }
      }).then((response) => {
        expect(response.status).to.eq(200);
      });
    });
  });
});

Sample Code Structure to Start On

A clear and well-organized code structure is crucial for writing maintainable API tests in Cypress. To help beginners get started, let's break this section into small steps, with sample code and detailed explanations.

Step 1: Setting Up Test Data

Test data is often stored in JSON files inside the cypress/fixtures folder. This allows you to reuse and manage data efficiently across multiple tests.

File: cypress/fixtures/sampleData.json
{
  "name": "John Doe",
  "email": "john.doe@example.com",
  "password": "securePassword123"
}

Explanation:
This file contains sample data to create a new user. The fields name, email, and password mimic the payload for an API request. Keeping data in fixtures ensures you can easily update or scale the test data.

Step 2: Adding Reusable Commands

Reusable commands help reduce redundancy in your tests. They are defined in cypress/support/commands.js.

File: cypress/support/commands.js
Cypress.Commands.add('createUser', () => {
  const baseUrl = Cypress.env('baseUrl'); // Assign baseUrl to a variable

  cy.fixture('userdata').then((userData) => {
    cy.request({
      method: 'POST',
      url: `${baseUrl}/${API_ENDPOINTS.USERS}`, // Use the baseUrl variable
      body: userData,
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${Cypress.env('apiKey')}`
      }
    }).then((response) => {
      expect(response.status).to.eq(201); // Validate that the user is created successfully
      Cypress.env('userId', response.body.id); // Save the created user's ID for later use
    });
  });
});

Cypress.Commands.add('getUser', () => {
  const baseUrl = Cypress.env('baseUrl'); // Assign baseUrl to a variable

  cy.fixture('userdata').then(() => {

    cy.request({
      method: 'GET',
      url: `${baseUrl}/${API_ENDPOINTS.USER}`, // Use the baseUrl variable
      headers: {
        Authorization: `Bearer ${Cypress.env('apiKey')}`
      }
    });
  });
});

Explanation:

  • createUser: Sends a POST request to create a new user and stores the user ID in Cypress's environment for use in subsequent tests.

  • getUser: Retrieves a user's details based on their user ID.

These commands encapsulate the request logic, making the test files cleaner and easier to read.

Step 3: Writing Test Cases

Let's write test cases that use reusable commands and test data.

File: cypress/integration/api/userTests.cy.js
describe('User Management API Tests', () => {
  const baseUrl = Cypress.env('baseUrl'); // Use baseUrl from the environment configuration

  it('Should create a new user', () => {
    cy.fixture('sampleData').then((userData) => {
      cy.request({
        method: 'POST',
        url: `${baseUrl}/${API_ENDPOINTS.USERS}`, // Use the baseUrl variable
        body: userData,
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${Cypress.env('apiKey')}`
        }
      }).then((response) => {
        expect(response.status).to.eq(201); // Validate successful user creation
        Cypress.env('userId', response.body.id); // Save user ID for later use
      });
    });
  });

  it('Should fetch the created user details', () => {

    cy.request({
      method: 'GET',
      url: `${baseUrl}/${API_ENDPOINTS.USER}`, // Use the baseUrl variable
      headers: {
        Authorization: `Bearer ${Cypress.env('apiKey')}`
      }
    }).then((response) => {
      expect(response.status).to.eq(200); // Validate a successful response
      expect(response.body.name).to.eq('John Doe'); // Validate the user's name
      expect(response.body.email).to.eq('john.doe@example.com'); // Validate the user's email
    });
  });

  it('Should update the user details', () => {

    const updatedData = { name: 'John Updated' };

    cy.request({
      method: 'PUT',
      url: `${baseUrl}/${API_ENDPOINTS.USER}`, // Use the baseUrl variable
      body: updatedData,
      headers: {
        Authorization: `Bearer ${Cypress.env('apiKey')}`
      }
    }).then((response) => {
      expect(response.status).to.eq(200); // Validate successful update
      expect(response.body.name).to.eq('John Updated'); // Validate updated name
    });
  });

  it('Should delete the user', () => {

    cy.request({
      method: 'DELETE',
      url: `${baseUrl}/${API_ENDPOINTS.USER}`, // Use the baseUrl variable
      headers: {
        Authorization: `Bearer ${Cypress.env('apiKey')}`
      }
    }).then((response) => {
      expect(response.status).to.eq(204); // Validate successful deletion
    });
  });
});

Explanation:

  1. Setup:

    • The before block sets the baseUrl and apiKey to simplify subsequent API calls.
  2. Test Flow:

    • Create a user: Reads the test data from the fixture file and creates a new user using createUser.

    • Fetch details: Retrieves the details of the created user and validates the response.

    • Update details: Sends a PUT request to update the user’s name and validates the update.

    • Delete user: Deletes the user and ensures a 204 No Content response.

Step 4: Organizing and Running Tests

  • Use meaningful file names (e.g., userTests.spec.js) to keep tests organized.

  • Run the tests with:

      npx cypress open
    

Conclusion

Building a strong foundational framework for Cypress API testing ensures the scalability and maintainability of your tests. By adhering to best practices and leveraging Cypress’s rich feature set, you can create reliable, efficient tests that integrate seamlessly into modern CI/CD workflows. Whether you are starting from scratch or enhancing an existing setup, this guide serves as your go-to resource for creating robust API tests with Cypress.