DEV Community

Cover image for πŸ”Œ Building Playwright Framework Step By Step - Implementing API Fixtures
idavidov13
idavidov13

Posted on • Edited on

πŸ”Œ Building Playwright Framework Step By Step - Implementing API Fixtures

🎯 Introduction

API (Application Programming Interface) testing is a fundamental aspect of the software testing process that focuses on verifying whether APIs meet functionality, reliability, performance, and security expectations! πŸš€ This type of testing is conducted at the message layer and involves sending calls to the API, getting outputs, and noting the system's response.

🌐 Why API Testing Matters: APIs are the backbone of modern applications - ensuring they work flawlessly is crucial for seamless user experiences!

🌟 Key Aspects of API Testing:

  • πŸ”§ Functionality Testing: Ensures that the API functions correctly and delivers expected outcomes in response to specific requests
  • πŸ›‘οΈ Reliability Testing: Verifies that the API can be consistently called upon and delivers stable performance under various conditions
  • ⚑ Performance Testing: Assesses the API's efficiency, focusing on response times, load capacity, and error rates under high traffic
  • πŸ”’ Security Testing: Evaluates the API's defense mechanisms against unauthorized access, data breaches, and vulnerabilities
  • πŸ”— Integration Testing: Ensures that the API integrates seamlessly with other services, platforms, and data, providing a cohesive user experience

πŸ’‘ Key Insight: API testing is crucial due to its ability to identify issues early in the development cycle, offering a more cost-effective and streamlined approach to ensuring software quality and security.

πŸ› οΈ Implement API Fixtures

πŸ“¦ Install zod Package

Zod is a TypeScript-first schema declaration and validation library that provides a powerful and elegant way to ensure data integrity throughout your application! 🎯 Unlike traditional validation libraries that solely focus on runtime validation, Zod integrates seamlessly with TypeScript, offering compile-time checks and type inference. This dual approach not only fortifies your application against incorrect data but also enhances developer experience by reducing the need for manual type definitions.

🎭 Why Zod?: Combines TypeScript's compile-time safety with runtime validation - the best of both worlds!

npm install zod
Enter fullscreen mode Exit fullscreen mode

πŸ“ Create 'api' Folder in the Fixtures Directory

This will be the central hub where we implement API fixtures and schema validation! πŸ—οΈ

πŸ—‚οΈ Organization Tip: Keeping API-related code in a dedicated folder improves maintainability and code organization.

πŸ”§ Create 'plain-function.ts' File

In this file, we'll encapsulate the API request process, managing all the necessary preparations before the request is sent and processing actions required after the response is obtained! βš™οΈ

πŸ’‘ Design Pattern: This helper function abstracts away the complexity of API requests, making your tests cleaner and more maintainable.

import type { APIRequestContext, APIResponse } from '@playwright/test';

/**
 * Simplified helper for making API requests and returning the status and JSON body.
 * This helper automatically performs the request based on the provided method, URL, body, and headers.
 *
 * @param {Object} params - The parameters for the request.
 * @param {APIRequestContext} params.request - The Playwright request object, used to make the HTTP request.
 * @param {string} params.method - The HTTP method to use (POST, GET, PUT, DELETE).
 * @param {string} params.url - The URL to send the request to.
 * @param {string} [params.baseUrl] - The base URL to prepend to the request URL.
 * @param {Record<string, unknown> | null} [params.body=null] - The body to send with the request (for POST and PUT requests).
 * @param {Record<string, string> | undefined} [params.headers=undefined] - The headers to include with the request.
 * @returns {Promise<{ status: number; body: unknown }>} - An object containing the status code and the parsed response body.
 *    - `status`: The HTTP status code returned by the server.
 *    - `body`: The parsed JSON response body from the server.
 */
export async function apiRequest({
    request,
    method,
    url,
    baseUrl,
    body = null,
    headers,
}: {
    request: APIRequestContext;
    method: 'POST' | 'GET' | 'PUT' | 'DELETE';
    url: string;
    baseUrl?: string;
    body?: Record<string, unknown> | null;
    headers?: string;
}): Promise<{ status: number; body: unknown }> {
    let response: APIResponse;

    const options: {
        data?: Record<string, unknown> | null;
        headers?: Record<string, string>;
    } = {};
    if (body) options.data = body;
    if (headers) {
        options.headers = {
            Authorization: `Token ${headers}`,
            'Content-Type': 'application/json',
        };
    } else {
        options.headers = {
            'Content-Type': 'application/json',
        };
    }

    const fullUrl = baseUrl ? `${baseUrl}${url}` : url;

    switch (method.toUpperCase()) {
        case 'POST':
            response = await request.post(fullUrl, options);
            break;
        case 'GET':
            response = await request.get(fullUrl, options);
            break;
        case 'PUT':
            response = await request.put(fullUrl, options);
            break;
        case 'DELETE':
            response = await request.delete(fullUrl, options);
            break;
        default:
            throw new Error(`Unsupported HTTP method: ${method}`);
    }

    const status = response.status();

    let bodyData: unknown = null;
    const contentType = response.headers()['content-type'] || '';

    try {
        if (contentType.includes('application/json')) {
            bodyData = await response.json();
        } else if (contentType.includes('text/')) {
            bodyData = await response.text();
        }
    } catch (err) {
        console.warn(
            `Failed to parse response body for status ${status}: ${err}`
        );
    }

    return { status, body: bodyData };
}
Enter fullscreen mode Exit fullscreen mode

πŸ“‹ Create schemas.ts File

In this file we will define all schemas by utilizing the powerful Zod schema validation library! 🎯

πŸ›‘οΈ Schema Benefits: Schemas ensure data consistency and catch type mismatches early, preventing runtime errors.

import { z } from 'zod';

export const UserSchema = z.object({
    user: z.object({
        email: z.string().email(),
        username: z.string(),
        bio: z.string().nullable(),
        image: z.string().nullable(),
        token: z.string(),
    }),
});

export const ErrorResponseSchema = z.object({
    errors: z.object({
        email: z.array(z.string()).optional(),
        username: z.array(z.string()).optional(),
        password: z.array(z.string()).optional(),
    }),
});

export const ArticleResponseSchema = z.object({
    article: z.object({
        slug: z.string(),
        title: z.string(),
        description: z.string(),
        body: z.string(),
        tagList: z.array(z.string()),
        createdAt: z.string(),
        updatedAt: z.string(),
        favorited: z.boolean(),
        favoritesCount: z.number(),
        author: z.object({
            username: z.string(),
            bio: z.string().nullable(),
            image: z.string(),
            following: z.boolean(),
        }),
    }),
});
Enter fullscreen mode Exit fullscreen mode

πŸ” Create types-guards.ts File

In this file, we're specifying the types essential for API Fixtures, as well as the types corresponding to various API responses we anticipate encountering throughout testing! πŸ“Š

🎯 TypeScript Power: Strong typing helps catch errors at compile time and provides excellent IDE support with autocomplete.

import { z } from 'zod';
import type {
    UserSchema,
    ErrorResponseSchema,
    ArticleResponseSchema,
} from './schemas';

/**
 * Parameters for making an API request.
 * @typedef {Object} ApiRequestParams
 * @property {'POST' | 'GET' | 'PUT' | 'DELETE'} method - The HTTP method to use.
 * @property {string} url - The endpoint URL for the request.
 * @property {string} [baseUrl] - The base URL to prepend to the endpoint.
 * @property {Record<string, unknown> | null} [body] - The request payload, if applicable.
 * @property {string} [headers] - Additional headers for the request.
 */
export type ApiRequestParams = {
    method: 'POST' | 'GET' | 'PUT' | 'DELETE';
    url: string;
    baseUrl?: string;
    body?: Record<string, unknown> | null;
    headers?: string;
};

/**
 * Response from an API request.
 * @template T
 * @typedef {Object} ApiRequestResponse
 * @property {number} status - The HTTP status code of the response.
 * @property {T} body - The response body.
 */
export type ApiRequestResponse<T = unknown> = {
    status: number;
    body: T;
};

// define the function signature as a type
export type ApiRequestFn = <T = unknown>(
    params: ApiRequestParams
) => Promise<ApiRequestResponse<T>>;

// grouping them all together
export type ApiRequestMethods = {
    apiRequest: ApiRequestFn;
};

export type User = z.infer<typeof UserSchema>;
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
export type ArticleResponse = z.infer<typeof ArticleResponseSchema>;
Enter fullscreen mode Exit fullscreen mode

🎭 Create api-request-fixtures.ts File

In this file we extend the test fixture from Playwright to implement our custom API fixture! πŸš€

πŸ”§ Fixture Pattern: Custom fixtures allow you to inject dependencies and setup code into your tests in a clean, reusable way.

import { test as base } from '@playwright/test';
import { apiRequest as apiRequestOriginal } from './plain-function';
import {
    ApiRequestFn,
    ApiRequestMethods,
    ApiRequestParams,
    ApiRequestResponse,
} from './types-guards';

export const test = base.extend<ApiRequestMethods>({
    /**
     * Provides a function to make API requests.
     *
     * @param {object} request - The request object.
     * @param {function} use - The use function to provide the API request function.
     */
    apiRequest: async ({ request }, use) => {
        const apiRequestFn: ApiRequestFn = async <T = unknown>({
            method,
            url,
            baseUrl,
            body = null,
            headers,
        }: ApiRequestParams): Promise<ApiRequestResponse<T>> => {
            const response = await apiRequestOriginal({
                request,
                method,
                url,
                baseUrl,
                body,
                headers,
            });

            return {
                status: response.status,
                body: response.body as T,
            };
        };

        await use(apiRequestFn);
    },
});
Enter fullscreen mode Exit fullscreen mode

πŸ”„ Update test-options.ts File

We need to add the API fixtures to the file, so we can use it in our test cases! 🎯

πŸ”— Integration: Merging fixtures allows you to use both page objects and API utilities in the same test seamlessly.

import { test as base, mergeTests, request } from '@playwright/test';
import { test as pageObjectFixture } from './page-object-fixture';
import { test as apiRequestFixture } from '../api/api-request-fixture';

const test = mergeTests(pageObjectFixture, apiRequestFixture);

const expect = base.expect;
export { test, expect, request };
Enter fullscreen mode Exit fullscreen mode

🎯 What's Next?

In the next article we will implement API Tests - putting our fixtures to work with real testing scenarios! πŸš€

πŸ’¬ Community: Please feel free to initiate discussions on this topic, as every contribution has the potential to drive further refinement.


✨ Ready to enhance your testing capabilities? Let's continue building this robust framework together!

Top comments (0)