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', () => {

  it('Should create a new user', () => {
      cy.request({
        method: 'POST',
        url: `${Cypress.env('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.request({
        method: 'POST',
        url: `${Cypress.env('baseUrl')}${API_ENDPOINTS.LOGIN}`,
        body: requestBody,
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${Cypress.env('apiKey')}`
        }
      }).then((response) => {
        expect(response.status).to.eq(200);
    });
  });
});
  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. Let's break this section into small steps, with sample code and detailed explanations to help beginners get started.

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', () => {
  cy.fixture('userdata').then((userData) => {
    cy.request({
      method: 'POST',
      url: `${Cypress.env('baseUrl')}/users`, // Use the base URL defined in your environment configuration
      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', () => {
  cy.fixture('userdata').then(() => {
    const userId = Cypress.env('userId'); // Retrieve the saved user ID from Cypress environment variables

    cy.request({
      method: 'GET',
      url: `${Cypress.env('baseUrl')}/users/${userId}`,
      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

Now, let's write test cases that use reusable commands and test data.

File: cypress/integration/api/userTests.cy.js
describe('User Management API Tests', () => {
  before(() => {
    // Set base URL and API key in Cypress environment variables
    Cypress.env('baseUrl', 'https://api.example.com');
    Cypress.env('apiKey', 'your-api-key');
  });

  it('Should create a new user', () => {
    cy.fixture('sampleData').then((userData) => {
      cy.createUser(userData); // Use the reusable command to create a user
    });
  });

  it('Should fetch the created user details', () => {
    const userId = Cypress.env('userId'); // Retrieve the user ID from the environment variable
    cy.getUser(userId).then((response) => {
      expect(response.status).to.eq(200); // Validate that the response is successful
      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 userId = Cypress.env('userId');
    const updatedData = { name: 'John Updated' };

    cy.request({
      method: 'PUT',
      url: `${Cypress.env('baseUrl')}/users/${userId}`,
      body: updatedData,
      headers: {
        Authorization: `Bearer ${Cypress.env('apiKey')}`
      }
    }).then((response) => {
      expect(response.status).to.eq(200);
      expect(response.body.name).to.eq('John Updated'); // Validate that the user's name is updated
    });
  });

  it('Should delete the user', () => {
    const userId = Cypress.env('userId');

    cy.request({
      method: 'DELETE',
      url: `${Cypress.env('baseUrl')}/users/${userId}`,
      headers: {
        Authorization: `Bearer ${Cypress.env('apiKey')}`
      }
    }).then((response) => {
      expect(response.status).to.eq(204); // Validate that the user is deleted successfully
    });
  });
});

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.

0
Subscribe to my newsletter

Read articles from Md. Abdullah Al Mamun directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Md. Abdullah Al Mamun
Md. Abdullah Al Mamun

👋 Hello, I'm Abdullah, a passionate advocate for innovation in software testing and quality assurance. With a background in FinTecdh, HealthTech, KYC, and Banking Solutions. I'm on a mission to share insights, strategies, and best practices that empower QA professionals and software enthusiasts alike. 🌐✨ Join me on a journey to explore the ever-evolving landscape of test automation, DevOps, and cutting-edge technologies. Through my blog, I aim to bridge the gap between theory and practical application, helping you stay at the forefront of software testing trends. 🚀 Let's connect, collaborate, and together, let's raise the bar for software quality assurance. Feel free to reach out for discussions, insights, or simply to share your thoughts on the world of QA. 📩🤝 #QA #TestAutomation #DevOps #QualityAssurance"