Category: api_testing

  • PactumJS Hands-On: Leverage stores for Authentication 

    PactumJS Hands-On: Leverage stores for Authentication 

    Introduction

    When testing APIs that require authentication or involve dependent requests, hardcoding tokens and dynamic values can quickly lead to fragile and hard-to-maintain tests. PactumJS offers a solution for this – stores, which allow you to capture and reuse values like tokens, IDs, and other response data.

    In this article, you’ll learn how to:

    • Handle authentication using Pactum stores
    • Chain requests by capturing and reusing dynamic values
    • Clean up test data using afterEach hooks

    Recap: POST Add Room request resulting 401 status code

    In the previous article, we created a test case Create a New Room but encountered a 401 Unauthorized error due to missing authentication:

    // tests/rooms.spec.js
    
    import pactum from 'pactum';
    const { spec, stash } = pactum;
    
    it('POST: Create a New Room', async () => {
        await spec()
            .post('/room')
            .withJson({ '@DATA:TEMPLATE@': 'RandomRoom' })
            .expectStatus(200)
            .expectJson({
                "success": true
            })
    })

    Since the /room endpoint requires authentication, we need to log in and attach a valid session token to our request.

    Storing and Reusing Tokens

    Pactum allows you to store response values and reuse them across requests using the .stores() method.

    To simulate authentication:

    await spec()
      .post('/auth/login')
      .withJson({ '@DATA:TEMPLATE@': 'ExistingUser' })
      .stores('token', 'token');

    This captures the token field from the login response and stores it under the key ‘token’.

    To use the stored token in subsequent requests:

    .withHeaders('Cookie', 'token=$S{token}')

    Chaining Requests

    You can also extract and store specific values like IDs from response bodies using the built-in json-query support in PactumJS. This allows you to query deeply nested JSON data with simple expressions.

    For example, to capture a roomId based on a dynamic roomName from the response:

    .stores('roomId', `rooms[roomName=${roomName}].roomid`);

    Then use it dynamically in future endpoints:

    .get('/room/$S{roomId}')

    Clean-Up Phase

    Cleaning up test data in afterEach ensures that your tests remain isolated and repeatable — a critical practice in CI/CD pipelines.

    In this example you can delete all the rooms, which have been created for the test:

    afterEach(async () => {
        await spec()
          .delete('/room/$S{roomId}')
          .withHeaders('Cookie', 'token=$S{token}');
      });

    Full Example: Creating a Room with Authentication

    Here’s a full test case demonstrating the use of authentication, value storage, and chaining:

    // tests/rooms.spec.js
    
    describe('POST Create a New Room', () => {
    
        beforeEach(async () => {
            await spec()
                .post('/auth/login')
                .withJson({
                    '@DATA:TEMPLATE@': 'ExistingUser'
                }).stores('token', 'token')
        });
    
    
        it('POST: Create a New Room', async () => {
            await spec()
                .post('/room')
                .inspect()
                .withHeaders('Cookie', 'token=$S{token}')
                .withJson({ '@DATA:TEMPLATE@': 'RandomRoom' })
                .expectStatus(200)
                .expectJson({
                    "success": true
                })
    
            const roomName = stash.getDataTemplate().RandomRoom.roomName;
    
            await spec()
                .get('/room')
                .inspect()
                .expectStatus(200)
                .stores('roomId', `rooms[roomName=${roomName}].roomid`);
    
            await spec()
                .get(`/room/$S{roomId}`)
                .inspect()
                .expectStatus(200)
                .expectJson('roomName', roomName);
        })
    
        afterEach(async () => {
            await spec()
                .delete('/room/$S{roomId}')
                .inspect()
                .withHeaders('Cookie', 'token=$S{token}')
        });
    
    })

    Understanding the Stash

    In the full example above, you may have noticed the use of stash.getDataTemplate():

    const roomName = stash.getDataTemplate().RandomRoom.roomName;

    The stash object in Pactum provides access to test data and stored values during runtime. Specifically, stash.getDataTemplate() allows you to retrieve values generated from the data template used earlier in .withJson({ ‘@DATA:TEMPLATE@’: ‘RandomRoom’ }).

    This is useful here to extract values from dynamically generated templates (like roomName) to use them in later requests.

    Bonus: Fetching Rooms without authentication

    Here’s a simple test for fetching all rooms without authentication:

    // tests/rooms.spec.js
    
    describe('GET: All Rooms', () => {
      it('should return all rooms', async () => {
        await spec()
          .get('/room')
          .expectStatus(200);
      });
    });

    Conclusion.

    Pactum’s store feature enables you to:

    • Authenticate without hardcoding credentials
    • Chain requests by dynamically storing and reusing values

    By combining this with beforeEach and afterEach hooks, you can effectively manage test preconditions and postconditions, ensuring your test cases remain clean, maintainable.

  • PactumJS in Practice: Using Data Templates to Manage Test Data – Part 1

    PactumJS in Practice: Using Data Templates to Manage Test Data – Part 1

    Introduction

    In this hands-on guide, we’ll explore how to improve the maintainability and flexibility of your API tests using data templates in PactumJS. Our focus will be on the authentication endpoint: POST /auth/login

    Recap: A Basic Login Test

    In the previous article we wrote a basic test case for a successful login:

    it('should succeed with valid credentials', async () => {
      await spec()
        .post('/auth/login')
        .inspect()
        .withJson({
          username: process.env.USERNAME,
          password: process.env.PASSWORD,
        })
        .expectStatus(200);
    });

    While this works for one case, hardcoding test data like this can quickly become difficult to manage as your test suite grows.

    Improving Test Maintainability with Data Templates

    To make our tests more scalable and easier to manage, we’ll introduce data templates — a PactumJS feature that allows you to centralize and reuse test data for different scenarios, such as valid and invalid logins.

    Step 1: Define Auth Templates

    Create a file auth.js inside your templates directory /helpers/datafactory/templates/ and register your authentication templates:

    // helpers/datafactory/templates/auth.js
    
    import pkg from 'pactum';
    const { stash } = pkg;
    import { faker } from '@faker-js/faker/locale/en';
    import dotenv from 'dotenv';
    dotenv.config();
    
    export function registerAuthTemplates() {
      stash.addDataTemplate({
        ExistingUser: {
            username: process.env.USERNAME,
            password: process.env.PASSWORD,
        },
        NonExistingUser: {
            username: 'non-existing-user',
            password: 'password',
        }
    });
    }
    

    Step 2: Register All Templates in a Central File

    Next, create a registerDataTemplates.js file to consolidate all your template registrations:

    //helpers/datafactory/templates/registerDataTemplates.js
    import { registerAuthTemplates } from "./auth.js";
    
    export function registerAllDataTemplates() {
        registerAuthTemplates();
        registerRoomTemplates();
      }

    Step 3: Use Templates in Your Test Setup

    Finally, import and register all templates in your test suite’s base configuration:

    // tests/base.js
    
    import pactum from 'pactum';
    import dotenv from 'dotenv';
    dotenv.config();
    import { registerAllDataTemplates } from '../helpers/datafactory/templates/registerDataTemplates.js';
    
    const { request } = pactum;
    
    before(() => {
      request.setBaseUrl(process.env.BASE_URL);
      registerAllDataTemplates()
    });
    

    Writing Login Tests with Templates

    Now let’s implement test cases for three core scenarios:

    // tests/auth.test.js
    
    describe('/auth/login', () => {
    
      it('should succeed with valid credentials', async () => {
        await spec()
          .post('/auth/login')
          .withJson({ '@DATA:TEMPLATE@': 'ExistingUser' })
          .expectStatus(200)
          .expectJsonSchema(authenticationSchema);
      });
    
      it('should fail with non-existing user', async () => {
        await spec()
          .post('/auth/login')
          .withJson({ '@DATA:TEMPLATE@': 'NonExistingUser' })
          .expectStatus(401)
          .expectJsonMatch('error', 'Invalid credentials');
      });
    
      it('should fail with invalid password', async () => {
        await spec()
          .post('/auth/login')
          .withJson({
            '@DATA:TEMPLATE@': 'ExistingUser',
            '@OVERRIDES@': {
              password: faker.internet.password(),
            },
          })
          .expectStatus(401)
          .expectJsonMatch('error', 'Invalid credentials');
      });
    
    });

    💡 Did You Know?

    You can use:

    • @OVERRIDES@ to override fields in your template (e.g. testing invalid passwords)
    • @REMOVES@ to remove fields from the payload (e.g. simulating missing inputs)

    Example:

    it('should return 400 when username is missing', async () => {
      await spec()
        .post('/auth/login')
        .withJson({
          '@DATA:TEMPLATE@': 'ExistingUser',
          '@REMOVES@': ['username']
        })
        .expectStatus(400);
    });

    Conclusion

    Data templates in PactumJS are a simple yet powerful way to make your API tests more maintainable and scalable. By centralizing test data, you reduce duplication, improve readability, and make your test suite easier to evolve as your API grows.

    In this part, we focused on authentication. In the next article, we’ll explore how to apply the same pattern to other endpoints — like POST /room — and build more complex test scenarios using nested data and dynamic generation.

  • Getting started with PactumJS: Project Structure and Your First Test Case

    Getting started with PactumJS: Project Structure and Your First Test Case

    Introduction

    As discussed in the previous article, PactumJS is an excellent choice for API automation testing. 

    As your API testing suite grows, maintaining a clean and organized repository structure becomes essential. We’ll explore a folder structure for your PactumJS-based testing framework, provide tips and tricks for configuration and scripting, and walk through executing tests with reporting.

    For demonstration, we’ll use the Restful Booker API as our test target.

    Set Up Your Project and Install Dependencies

    Prerequisites

    To follow along, make sure you have the following:

    1. Node.js v10 or above
    2. Basic understanding of JavaScript or TypeScript
    3. Node.js modules
    4. Testing frameworks like Mocha

    If you’re new to any of the above, it’s worth reviewing basic tutorials, for example, on Automation University: on Node.js and test runners like Mocha.

    Install Dependencies

    Start by creating a fresh Node.js project:

    mkdir api_testing_with_pactumjs
    cd api_testing_with_pactumjs
    npm init -y

    Then install necessary packages via NPM:

    # install pactum
    npm install -D pactum
    
    # install a test runner
    npm install -D mocha

    Organise your files

    api_testing_with_pactumjs/
    ├── helpers/
    │   └── datafactory/
    ├── tests/
    │   └── auth.spec.ts
    ├── setup/
    │   └── base.js
    ├── .env.example
    ├── .gitignore
    ├── README.md
    ├── package-lock.json
    └── package.json
    1. tests/ folder contains your test specifications organized by feature or endpoint, such as auth.spec.ts. This keeps tests modular and easy to locate.
    2. helpers/ folder houses centralized reusable logic and utilities. This separation keeps test files focused on what they test rather than how, improving readability and maintainability.
    3. setup/ folder contains global setup files like base.js to configure common test environment settings, such as base URLs and global hooks.
    4. .env.example — A sample environment configuration file listing required environment variables, serving as a reference and template for developers.
    5. .env (not shown in repo) is used locally to store sensitive configuration and secrets, enabling easy environment switching without code changes.
    6. .gitignore file includes folders and files like .env to prevent committing sensitive data to version control.
    7. package.json is a central place for managing project dependencies (like pactum, dotenv, mocha) and defining test scripts (e.g., npm run test, npm run test:report). This facilitates CI/CD integration and consistent test execution.

    Write a Basic Test

    As an example for our demo we will take the Restful-Booker Platform built by Mark Winteringham. This application has been created for bed-and-breakfast (B&B) owners to manage their bookings.

    To explore and test the available API endpoints, you can use the official Postman Collection.

    Let’s write our first set of API tests for the /auth/login endpoint which generates a token for an admin user.

    Endpoint: POST /api/auth/login

    Base URL: https://automationintesting.online

    User Context

    User Role: Admin (default user)

    Credentials Used:

    • username: “admin”
    • password: “password”

    Request:

    Method: POST

    Headers: Content-Type: application/json

    Body:

    {
      "username": "admin",
      "password": "password"
    }

    Expected Response:

    HTTP Status: 200 OK

    / tests/authenticate.spec.js
    import pkg from 'pactum';
    const { spec, stash } = pkg;
    
    describe('/authenticate', () => {
    
        it('should succeed with valid credentials', async () => {
            await spec()
                .post('https://automationintesting.online/api/auth/login')
                .withJson({
                    username: 'admin',
                    password: 'password'
                })
                .expectStatus(200)
        });
    });

    While this test currently focuses on verifying the status code, future articles will enhance it by adding validations for the authentication token returned in the response.

    Manage Environment Variables

    Create .env file

    To keep sensitive data like URLs and credentials, create a .env.example file as a reference for required environment variables:

    BASE_URL=""
    USERNAME=""
    PASSWORD=""
    👉 Tip: Don’t commit your actual .env to version control
    • Use .env.example to document the required variables.
    • Add .env to your .gitignore file to keep credentials secure.
    • Share .env.example with your team so they can configure their environments consistently.

    Load Environment Variables in Tests

    Install dotenv and configure it in your test files or setup scripts:

    npm install --save-dev dotenv

    Example test with environment variables:

    // tests/authenticate.spec.js
    
    import pkg from 'pactum';
    const { spec } = pkg;
    import dotenv from 'dotenv';
    dotenv.config();
    
    describe('/authenticate', () => {
      it('should succeed with valid credentials', async () => {
        await spec()
          .post(`${process.env.BASE_URL}/auth/login`)
          .withJson({
            username: process.env.USERNAME,
            password: process.env.PASSWORD
          })
          .expectStatus(200);
      });
    });

    Execute Test Case

    Once your test files are set up and your .env file is configured with valid credentials and base URL, you’re ready to execute your test cases.

    PactumJS works seamlessly with test runners like Mocha, which means running your tests is as simple as triggering a test command defined in your package.json. Here’s how to proceed:

    Add a Test Script

    In your package.json, add a script under “scripts” to define how to run your tests. For example:

    // package.json
    
    "scripts": {
      "test": "mocha tests"
    }

    This tells Mocha to look for test files in the tests/ directory and run them.

    Run the Tests

    In your terminal, from the root of your project, run:

    npm test

    This will execute test specs and display results in the terminal. 

    You should see output indicating whether the test passed or failed, for example:

      /authenticate
        ✓ should succeed with valid credentials (150ms)
    
      1 passing (151ms)

    Add a Reporting Tool

    By default, PactumJS uses Mocha’s basic CLI output. For richer reporting—especially useful in CI/CD pipelines—you can use Mochawesome, a popular HTML and JSON reporter for Mocha.

    Install Mochawesome

    Install Mochawesome as a development dependency:

    npm install -D mochawesome

    Update Your Test Script

    Modify the scripts section in your package.json to include a command for generating reports:

    // package.json
    
    "scripts": {
      "test": "mocha tests"
      "test:report": "mocha tests --reporter mochawesome"
    }

    This script tells Mocha to run your tests using the Mochawesome reporter.

    Run the tests with reporting

    Execute your tests using the new script:

    npm run test:report

    This generates a mocha report in JSON and HTML format which you can review locally or attach in CI pipelines.

      /authenticate
        ✔ should succeed with valid credentials (364ms)
    
    
      1 passing (366ms)
    
    [mochawesome] Report JSON saved to ./pactum_test/mochawesome-report/mochawesome.json  [mochawesome] Report HTML saved to ./pactum_test/mochawesome-report/mochawesome.html

    View the report

    Open the HTML report in your browser to visually inspect test results:

    Configure Base Test Setup (base.js)

    Create a Shared Configuration

    Create a base.js file in the setup/ directory. This file is a shared configuration used to define reusable logic like setting the base URL, request headers, or global hooks (beforeEach, afterEach). 

    // setup/base.js
    
    import pactum from 'pactum';
    import dotenv from 'dotenv';
    dotenv.config();
    
    const { request } = pactum;
    
    before(() => {
      request.setBaseUrl(process.env.BASE_URL);
    });

    Load the Setup Automatically Using –file

    To ensure this configuration runs before any tests, register the setup file using Mocha’s –file option. This guarantees Mocha will execute base.js within its context, making all Mocha globals (like before) available.

    Example package.json script:

    "scripts": {
      "test": "mocha tests --file setup/base.js"
    }

    With this in place, run:

    npm test
    👉 Tip: Simplify and DRY Up Your Test Scripts

    To avoid repeating the full Mocha command in multiple scripts, define a single base script (e.g., test) that includes your common options. Then, reuse it for other variants by passing additional flags:

    "scripts": {
      "test": "mocha tests --file setup/base.js",
      "test:report": "npm run test -- --reporter mochawesome"
    }

    This approach keeps your scripts concise and easier to maintain by centralizing the core test command. It also allows you to easily extend or customize test runs with additional options without duplicating configuration. Overall, it reduces the chance of errors and inconsistencies when updating your test scripts.

    Conclusion

    By structuring your PactumJS repository with clear separation of tests, helpers, and setup files—and by leveraging environment variables, global setup, and reporting—you build a scalable and maintainable API testing framework. This approach supports growth, team collaboration, and integration with CI/CD pipelines.

  • What makes PactumJS awesome? A quick look at its best features.

    What makes PactumJS awesome? A quick look at its best features.

    1. Introduction
      1. Fluent and expressive syntax
      2. Data Management
        1. Data Templates
        2. Data Store for Dynamic Values
      3. Built-In Schema Validation
      4. Flexible Assertions
      5. Default Configuration 
    2. Conclusion
    3. Resources

    Introduction

    I’ve spent a fair bit of time writing API test automation. After exploring a few JavaScript-based tools and libraries, I’ve found Pactum to be particularly powerful. I wanted to take a moment to share a brief overview of my experience and why I think it stands out.

    If you’re setting up a PactumJS project from scratch, I recommend starting with the official Quick Start guide, which covers installation and basic setup clearly. Additionally, this article by Marie Cruz offers a great walkthrough of writing API tests with PactumJS and Jest, especially useful for beginners.

    Fluent and expressive syntax

    One of the aspects I appreciate the most is how naturally you can chain descriptive methods from the spec object to build complex requests with support for headers, body payloads, query parameters, and more.

    Example: 

    it('POST with existing username and valid password', async () => {
            await spec()
                .post('/auth/login')             .inspect()
                .withHeaders('Content-Type', 'application/json')
                .withJson({
                    '@DATA:TEMPLATE@': 'ExistingUser'
                })
                .expectStatus(200) # assertion
                .expectJsonSchema(authenticationSchema) # assertion
        })
    

    More on request making: https://github.com/pactumjs/pactum/wiki/API-Testing#request-making 

    Data Management

    Data Management is a critical aspect of test automation and often one of the more challenging pain points in any automation project. Test suites frequently reuse similar request payloads, making it difficult to maintain and organize these payloads when they are scattered across different test files or folders. Without a structured approach, this can lead to duplication, inconsistency, and increased maintenance overhead. So, it is important to have an intuitive way to handle data in the test framework. 

    In PactumJS, data management is typically handled using data templates and data stores. These help you define reusable request bodies, dynamic data, or test user information in a clean and maintainable way.

    Data Templates

    Data Templates help you define reusable request bodies and user credentials. Templates can also be locally customized within individual tests without affecting the original definition.

    For example, in testing different authentication scenarios:

    1. Valid credentials
    2. Invalid password
    3. Non-existing user

    Rather than hard-coding values in each test, as it is done below: 

    describe('/authenticate', () => {
        it('POST with existing username and valid password', async () => {
            await spec()
                .post('/auth/login')
                .inspect()
                .withHeaders('Content-Type', 'application/json')
                .withJson({
                    username: process.env.USERNAME,
                    password: process.env.PASSWORD,
                })
                .expectStatus(200)
                .expectJsonSchema(authenticationSchema)
        })
    
        it('POST with existing username and invalid password', async () => {
            await spec()
                .post('/auth/login')
                .inspect()
                .withHeaders('Content-Type', 'application/json')
                .withJson({
                    username: process.env.USERNAME,
                    password: faker.internet.password(),
                })
                .expectStatus(401)
                .expectJsonMatch('error', 'Invalid credentials')
        })
    
        it('POST with non-existing username and password', async () => {
            await spec()
                .post('/auth/login')
                .inspect()
                .withHeaders('Content-Type', 'application/json')
                .withJson({
                    username: faker.internet.username(),
                    password: faker.internet.password(),
                })
                .expectStatus(401)
                .expectJsonMatch('error', 'Invalid credentials')
        })
    })

    define reusable templates:

    // auth.js
    
    export function registerAuthTemplates() {
      stash.addDataTemplate({
        ExistingUser: {
            username: process.env.USERNAME,
            password: process.env.PASSWORD,
        },
        NonExistingUser: {
            username: faker.internet.username(),
            password: faker.internet.password(),
        }
    });
    }

    Then load them in global setup:

    // registerDataTemplates.js
    
    import { registerAuthTemplates } from "./auth.js";
    
    export function registerAllDataTemplates() {
        registerAuthTemplates();
      }

    Now, tests become cleaner and easier to maintain:

     it('POST with non-existing username and password', async () => {
            await spec()
                .post('/auth/login')
                .inspect()
                .withHeaders('Content-Type', 'application/json')
                .withJson({
                    '@DATA:TEMPLATE@': 'NonExistingUser'
                })
                .expectStatus(401)
                .expectJsonMatch('error', 'Invalid credentials')
        })

    Want to override part of a template? 

    Use @OVERRIDES@:

     it('POST with existing username and invalid password', async () => {
            await spec()
                .post('/auth/login')
                .inspect()
                .withHeaders('Content-Type', 'application/json')
                .withJson({
                    '@DATA:TEMPLATE@': 'ExistingUser',
                    '@OVERRIDES@': {
                        'password': faker.internet.password()
                      }
                })
                .expectStatus(401)
                .expectJsonMatch('error', 'Invalid credentials')
        })

    This approach improves consistency and reduces duplication. When credential details change, updates can be made centrally in the datafactory without touching individual tests. As a result, test logic remains clean, focused on validating behaviour rather than being cluttered with data setup.

    More information on data templates: https://pactumjs.github.io/guides/data-management.html#data-template 

    Data Store for Dynamic Values

    In integration and e2e API testing, one common challenge is managing dynamic data between requests. For example, you might need to extract an authentication token from an authentication response and use it in the header of subsequent requests. Without a clean way to store and reuse this data, tests can become messy, brittle, and hard to maintain.

    PactumJS provides a data store feature that allows you to save custom response data during test execution in a clean way.

    Example:

    Suppose you want to send a POST request to create a room, but the endpoint requires authentication. First, you make an authentication request and receive a token in the response. Using data store functionality, you can capture and store this token, then inject it into the headers of the room creation request. 

    describe('POST Create a New Room', () => {
    
        beforeEach(async () => {
            await spec()
                .post('/auth/login')
                .withHeaders('Content-Type', 'application/json')
                .withJson({
                    '@DATA:TEMPLATE@': 'ExistingUser'
                }).stores('token', 'token')
        });
    
    
        it('POST: Create a New Room', async () => {
            await spec()
                .post('/room')
                .inspect()
                .withHeaders('Content-Type', 'application/json')
                .withHeaders('Cookie', 'token=$S{token}')
                .withJson({ '@DATA:TEMPLATE@': 'RandomRoom' })
                .expectStatus(200)
                .expectBody({
                    "success": true
                })
    
    })

    Data store functionality also supports json-query libraries. It enables you to extract and store specific values from complex JSON responses. This is particularly helpful when dealing with nested structures, where you only need to capture a portion of the response—such as an ID, token, or status—from a larger payload.

    Example:

    await spec()
                .get('/room')
                .inspect()
                .withHeaders('Content-Type', 'application/json')
                .expectStatus(200)
                .stores('roomId', `rooms[roomName=${roomName}].roomid`);
    
            await spec()
                .get(`/room/$S{roomId}`)
                .inspect()
                .withHeaders('Content-Type', 'application/json')
                .expectStatus(200)
                .expectJson('roomName', roomName);
        })

    More on data store: https://pactumjs.github.io/guides/data-management.html#data-store 

    Built-In Schema Validation

    Unlike other setups that require integrating libraries like zod, ajv, or custom helper functions, PactumJS allows you to validate JSON responses using the expectJsonSchema method. All you need to do is define the expected schema and apply it directly in your test, no extra configuration needed.

    For example, in an authentication test case, the response schema is defined in a separate data factory:

    export const authenticationSchema = {
        "type": "object",
        "properties": {
            "token": {
                "type": "string"
            }
        },
        "additionalProperties": false,
        "required": ["token"]
    }

    You can then validate the structure of the response like this:

    it('POST with existing username and valid password', async () => {
            await spec()
                .post('/auth/login')
                .inspect()
                .withHeaders('Content-Type', 'application/json')
                .withJson({
                    '@DATA:TEMPLATE@': 'ExistingUser'
                })
                .expectStatus(200)
                .expectJsonSchema(authenticationSchema)
        })

    Flexible Assertions

    Most REST API responses return data in JSON format that must be validated. Fortunately, PactumJS provides a powerful and expressive assertion system that goes far beyond basic status code checks. Its assertion system allows for 

    1. Deep JSON matching:
    await spec()
        .get(`/room/$S{roomId}`)
        .inspect()
        .expectStatus(200)
        .expectJson('roomName', roomName);
    })
    it('POST with non-existing username and password', async () => {
         await spec()
            .post('/auth/login')
            .inspect()
            .withJson({
                '@DATA:TEMPLATE@': 'NonExistingUser'
            })
            .expectStatus(401)
            .expectJsonMatch('error', 'Invalid credentials')
        })
    1. Partial comparisons:
    it('posts should have a item with title -"some title"', async () => {
      const response = await pactum.spec()
        .get('https://jsonplaceholder.typicode.com/posts')
        .expectStatus(200)
        .expectJsonLike([
          {
            "userId": /\d+/,
            "title": "some title"
          }
        ]);
    });
    1. Path-Based Validation:
    it('get people', async () => {
      const response = await pactum.spec()
        .get('https://some-api/people')
        .expectStatus(200)
        .expectJson({
          people: [
            { name: 'Matt', country: 'NZ' },
            { name: 'Pete', country: 'AU' },
            { name: 'Mike', country: 'NZ' }
          ]
        })
        .expectJsonAt('people[country=NZ].name', 'Matt')
        .expectJsonAt('people[*].name', ['Matt', 'Pete', 'Mike']);
    });
    1. Dynamic Runtime Expressions:
    it('get users', async () => {
      await pactum.spec()
        .get('/api/users')
        .expectJsonLike('$V.length === 10'); // api should return an array with length 10
        .expectJsonLike([
          {
            id: 'typeof $V === "string"',
            name: 'jon',
            age: '$V > 30' // age should be greater than 30
          }
        ]);
    });

    And all of them are in a clean and readable format. 

    For example, you can validate only parts of a response, use regex or custom matchers, and even plug in JavaScript expressions or reusable assertion handlers. In my opinion, this level of granularity is a game-changer compared to assertion styles in other frameworks.

    Check more in the official documentation: https://github.com/pactumjs/pactum/wiki/API-Testing#response-validation 

    Default Configuration 

    To reduce repetition and keep tests clean, PactumJS allows you to define default values that apply globally across your test suite — such as headers, base URL, and request timeouts. This helps maintain consistency and simplifies test configuration.

    Here’s how it can be implemented:

    before(() => {
      request.setBaseUrl(process.env.BASE_URL);
      request.setDefaultHeaders('Content-Type', 'application/json');
    });

    More information you can find here: https://github.com/pactumjs/pactum/wiki/API-Testing#request-settings 

    Conclusion

    In my experience, PactumJS has proven to be a well-designed and developer-friendly tool for API test automation. Its fluent syntax, robust data handling, and built-in features like schema validation and dynamic stores eliminate the need for developing third-party solutions for the test framework.

    If you’re working with API testing in JavaScript / Typescript, PactumJS is definitely worth a look.

    Resources

    1. You can find the complete set of test cases, data templates, and helper functions shown in this post in the GitHub Repo
    2. Official PactumJS Documentation: https://pactumjs.github.io/ 
    3. PactumJS WiKi Page: https://github.com/pactumjs/pactum/wiki/API-Testing 
    4. Code Examples in PactumJS GitHub: https://github.com/pactumjs/pactum-examples