๐ 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
๐ 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 };
}
๐ 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(),
}),
}),
});
๐ 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>;
๐ญ 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);
},
});
๐ 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 };
๐ฏ 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!
Subscribe to my newsletter
Read articles from Ivan Davidov directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ivan Davidov
Ivan Davidov
Automation QA Engineer, ISTQB CTFL, PSM I, helping teams improve the quality of the product they deliver to their customers. โข Led the development of end-to-end (E2E) and API testing frameworks from scratch using Playwright and TypeScript, ensuring robust and scalable test automation solutions. โข Integrated automated tests into CI/CD pipelines to enhance continuous integration and delivery. โข Created comprehensive test strategies and plans to improve test coverage and effectiveness. โข Designed performance testing frameworks using k6 to optimize system scalability and reliability. โข Provided accurate project estimations for QA activities, aiding effective project planning. โข Worked with development and product teams to align testing efforts with business and technical requirements. โข Improved QA processes, tools, and methodologies for increased testing efficiency. โข Domain experience: banking, pharmaceutical and civil engineering. Bringing over 3 year of experience in Software Engineering, 7 years of experience in Civil engineering project management, team leadership and project design, to the table, I champion a disciplined, results-driven approach, boasting a record of more than 35 successful projects.