🎯 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 | null} [params.body=null] - The body to send with the request (for POST and PUT requests).
* @param {Record | 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 | 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!