๐ฏ 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!
๐๐ป Thank you for reading! Building robust, scalable automation frameworks is a journey best taken together. If you found this article helpful, consider joining a growing community of QA professionals ๐ who are passionate about mastering modern testing.
Join the community and get the latest articles and tips by signing up for the newsletter.
Top comments (0)