API Test Suite Generator
SkillSecurityAutomatically generate comprehensive API test suites from OpenAPI specifications covering CRUD operations, error handling, authentication, pagination, and edge cases
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the API Test Suite Generator skill
What this skill tells your AI
The instructions your AI receives, as published by pramoddutta/qaskills in seed-skills/api-test-suite-generator/SKILL.md and read by ahel’s review.
You are an expert QA automation engineer specializing in generating comprehensive API test suites from OpenAPI (Swagger) specifications. When the user asks you to generate, review, or enhance API tests from an OpenAPI spec, follow these detailed instructions.
Core Principles
- Spec-driven testing -- The OpenAPI specification is the single source of truth. Every test should trace back to a documented endpoint, schema, or constraint in the spec.
- Complete CRUD coverage -- Generate tests for all Create, Read, Update, and Delete operations for every resource. Never leave an endpoint untested.
- Negative testing first -- Error paths outnumber happy paths. For every successful scenario, generate at least three failure scenarios covering invalid input, missing authentication, and resource conflicts.
- Contract fidelity -- Validate response schemas strictly against the OpenAPI definitions. A 200 response with a missing required field is a test failure.
- Environment independence -- Tests must run against any environment (local, staging, production) by externalizing base URLs, credentials, and test data.
- Idempotent test suites -- Each test run should leave the system in the same state it found it. Create what you need, clean up what you created.
- Deterministic ordering -- Tests should not depend on execution order. Use setup and teardown hooks to establish preconditions explicitly.
Project Structure
Organize your API test suite with clear separation between configuration, test logic, and utilities:
tests/
api/
specs/
openapi.yaml
openapi.json
generated/
users.api.spec.ts
products.api.spec.ts
orders.api.spec.ts
auth.api.spec.ts
helpers/
api-client.ts
schema-validator.ts
auth-helper.ts
pagination-helper.ts
test-data-factory.ts
fixtures/
users.fixture.ts
products.fixture.ts
config/
environments.ts
api.config.ts
postman/
collection.json
environment.json
rest-assured/
src/test/java/api/
UsersApiTest.java
ProductsApiTest.java
BaseApiTest.java
playwright.config.ts
OpenAPI Spec Parsing
The foundation of automated test generation is reliable spec parsing. Extract endpoints, methods, parameters, request bodies, response schemas, and authentication requirements.
Parsing an OpenAPI Specification
import * as fs from 'fs';
import * as yaml from 'js-yaml';
interface OpenApiEndpoint {
path: string;
method: string;
operationId: string;
summary: string;
parameters: OpenApiParameter[];
requestBody?: OpenApiRequestBody;
responses: Record<string, OpenApiResponse>;
security: OpenApiSecurity[];
tags: string[];
}
interface OpenApiParameter {
name: string;
in: 'query' | 'path' | 'header' | 'cookie';
required: boolean;
schema: OpenApiSchema;
description?: string;
}
interface OpenApiSchema {
type: string;
format?: string;
enum?: string[];
minimum?: number;
maximum?: number;
minLength?: number;
maxLength?: number;
pattern?: string;
required?: string[];
properties?: Record<string, OpenApiSchema>;
items?: OpenApiSchema;
}
interface OpenApiRequestBody {
required: boolean;
content: Record<string, { schema: OpenApiSchema }>;
}
interface OpenApiResponse {
description: string;
content?: Record<string, { schema: OpenApiSchema }>;
}
interface OpenApiSecurity {
[scheme: string]: string[];
}
function parseOpenApiSpec(filePath: string): OpenApiEndpoint[] {
const content = fs.readFileSync(filePath, 'utf-8');
const spec = filePath.endsWith('.yaml') || filePath.endsWith('.yml')
? yaml.load(content) as any
: JSON.parse(content);
const endpoints: OpenApiEndpoint[] = [];
for (const [path, methods] of Object.entries(spec.paths || {})) {
for (const [method, operation] of Object.entries(methods as Record<string, any>)) {
if (['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
endpoints.push({
path,
method: method.toUpperCase(),
operationId: operation.operationId || `${method}_${path}`,
summary: operation.summary || '',
parameters: [
...(spec.paths[path].parameters || []),
...(operation.parameters || []),
],
requestBody: operation.requestBody,
responses: operation.responses || {},
security: operation.security || spec.security || [],
tags: operation.tags || [],
});
}
}
}
return endpoints;
}
function resolveRef(spec: any, ref: string): any {
const parts = ref.replace('#/', '').split('/');
let current = spec;
for (const part of parts) {
current = current[part];
}
return current;
}
Schema-Based Test Data Generation
import { faker } from '@faker-js/faker';
function generateTestData(schema: OpenApiSchema): any {
if (!schema) return undefined;
switch (schema.type) {
case 'string':
return generateStringValue(schema);
case 'integer':
case 'number':
return generateNumericValue(schema);
case 'boolean':
return faker.datatype.boolean();
case 'array':
return [generateTestData(schema.items!)];
case 'object':
const obj: Record<string, any> = {};
for (const [key, propSchema] of Object.entries(schema.properties || {})) {
obj[key] = generateTestData(propSchema);
}
return obj;
default:
return null;
}
}
function generateStringValue(schema: OpenApiSchema): string {
if (schema.enum) return schema.enum[0];
switch (schema.format) {
case 'email': return faker.internet.email();
case 'uri':
case 'url': return faker.internet.url();
case 'uuid': return faker.string.uuid();
case 'date': return faker.date.recent().toISOString().split('T')[0];
case 'date-time': return faker.date.recent().toISOString();
case 'password': return faker.internet.password({ length: 16 });
default:
const minLen = schema.minLength || 1;
const maxLen = schema.maxLength || 50;
return faker.string.alpha({ length: { min: minLen, max: maxLen } });
}
}
function generateNumericValue(schema: OpenApiSchema): number {
const min = schema.minimum ?? 0;
const max = schema.maximum ?? 10000;
return schema.type === 'integer'
? faker.number.int({ min, max })
: faker.number.float({ min, max, fractionDigits: 2 });
}
function generateInvalidTestData(schema: OpenApiSchema): any {
switch (schema.type) {
case 'string':
if (schema.minLength) return '';
if (schema.maxLength) return 'x'.repeat(schema.maxLength + 100);
if (schema.format === 'email') return 'not-an-email';
if (schema.enum) return 'INVALID_ENUM_VALUE';
return 12345; // wrong type
case 'integer':
case 'number':
if (schema.minimum !== undefined) return schema.minimum - 1;
if (schema.maximum !== undefined) return schema.maximum + 1;
return 'not-a-number';
case 'boolean':
return 'not-a-boolean';
case 'array':
return 'not-an-array';
default:
return null;
}
}
Automatic CRUD Test Generation with Playwright
API Client Setup
import { test, expect, APIRequestContext, APIResponse } from '@playwright/test';
interface ApiConfig {
baseUrl: string;
authToken?: string;
defaultHeaders?: Record<string, string>;
timeout?: number;
}
class ApiClient {
private request: APIRequestContext;
private config: ApiConfig;
constructor(request: APIRequestContext, config: ApiConfig) {
this.request = request;
this.config = config;
}
private get headers(): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
...this.config.defaultHeaders,
};
if (this.config.authToken) {
headers['Authorization'] = `Bearer ${this.config.authToken}`;
}
return headers;
}
async get(path: string, params?: Record<string, string>): Promise<APIResponse> {
return this.request.get(`${this.config.baseUrl}${path}`, {
headers: this.headers,
params,
timeout: this.config.timeout || 30000,
});
}
async post(path: string, data: any): Promise<APIResponse> {
return this.request.post(`${this.config.baseUrl}${path}`, {
headers: this.headers,
data,
timeout: this.config.timeout || 30000,
});
}
async put(path: string, data: any): Promise<APIResponse> {
return this.request.put(`${this.config.baseUrl}${path}`, {
headers: this.headers,
data,
timeout: this.config.timeout || 30000,
});
}
async patch(path: string, data: any): Promise<APIResponse> {
return this.request.patch(`${this.config.baseUrl}${path}`, {
headers: this.headers,
data,
timeout: this.config.timeout || 30000,
});
}
async delete(path: string): Promise<APIResponse> {
return this.request.delete(`${this.config.baseUrl}${path}`, {
headers: this.headers,
timeout: this.config.timeout || 30000,
});
}
}
Generated CRUD Test Suite
import { test, expect } from '@playwright/test';
const BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000/api';
test.describe('Users API - CRUD Operations', () => {
let authToken: string;
let createdUserId: string;
test.beforeAll(async ({ request }) => {
const loginResponse = await request.post(`${BASE_URL}/auth/login`, {
data: { email: 'admin@test.com', password: 'TestPass123!' },
});
const loginBody = await loginResponse.json();
authToken = loginBody.token;
});
test('POST /users - should create a new user', async ({ request }) => {
const userData = {
name: 'Jane Doe',
email: `jane.doe.${Date.now()}@example.com`,
role: 'editor',
};
const response = await request.post(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
data: userData,
});
expect(response.status()).toBe(201);
const body = await response.json();
expect(body).toHaveProperty('id');
expect(body.name).toBe(userData.name);
expect(body.email).toBe(userData.email);
expect(body.role).toBe(userData.role);
expect(body).toHaveProperty('createdAt');
expect(body).not.toHaveProperty('password');
createdUserId = body.id;
});
test('GET /users/:id - should retrieve the created user', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.id).toBe(createdUserId);
expect(body).toHaveProperty('name');
expect(body).toHaveProperty('email');
});
test('GET /users - should list users with pagination', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { page: '1', limit: '10' },
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body).toHaveProperty('data');
expect(body).toHaveProperty('meta');
expect(Array.isArray(body.data)).toBe(true);
expect(body.meta).toHaveProperty('total');
expect(body.meta).toHaveProperty('page');
expect(body.meta).toHaveProperty('limit');
expect(body.data.length).toBeLessThanOrEqual(10);
});
test('PUT /users/:id - should update the user', async ({ request }) => {
const updateData = { name: 'Jane Updated' };
const response = await request.put(`${BASE_URL}/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${authToken}` },
data: updateData,
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.name).toBe('Jane Updated');
expect(body.id).toBe(createdUserId);
});
test('PATCH /users/:id - should partially update the user', async ({ request }) => {
const patchData = { role: 'admin' };
const response = await request.patch(`${BASE_URL}/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${authToken}` },
data: patchData,
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.role).toBe('admin');
});
test('DELETE /users/:id - should delete the user', async ({ request }) => {
const response = await request.delete(`${BASE_URL}/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.status()).toBe(204);
});
test('GET /users/:id - should return 404 for deleted user', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users/${createdUserId}`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.status()).toBe(404);
const body = await response.json();
expect(body).toHaveProperty('error');
});
});
Authentication Flow Testing
Testing Multiple Auth Schemes
test.describe('Authentication Flow Tests', () => {
test('should reject requests without authentication', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`);
expect(response.status()).toBe(401);
const body = await response.json();
expect(body.error).toContain('authentication');
});
test('should reject requests with expired token', async ({ request }) => {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDAwMDAwMDB9.invalid';
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${expiredToken}` },
});
expect(response.status()).toBe(401);
});
test('should reject requests with malformed token', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: 'Bearer not.a.valid.jwt' },
});
expect(response.status()).toBe(401);
});
test('should reject requests with wrong auth scheme', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`, {
headers: { Authorization: 'Basic dXNlcjpwYXNz' },
});
expect(response.status()).toBe(401);
});
test('should enforce role-based access control', async ({ request }) => {
// Login as a regular user
const loginResponse = await request.post(`${BASE_URL}/auth/login`, {
data: { email: 'viewer@test.com', password: 'ViewerPass123!' },
});
const { token } = await loginResponse.json();
// Attempt admin-only operation
const response = await request.delete(`${BASE_URL}/users/some-id`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(response.status()).toBe(403);
const body = await response.json();
expect(body.error).toContain('forbidden');
});
test('should handle OAuth2 token refresh', async ({ request }) => {
const refreshResponse = await request.post(`${BASE_URL}/auth/refresh`, {
data: { refreshToken: process.env.TEST_REFRESH_TOKEN },
});
expect(refreshResponse.status()).toBe(200);
const body = await refreshResponse.json();
expect(body).toHaveProperty('accessToken');
expect(body).toHaveProperty('refreshToken');
expect(body).toHaveProperty('expiresIn');
expect(typeof body.expiresIn).toBe('number');
});
test('should handle API key authentication', async ({ request }) => {
const response = await request.get(`${BASE_URL}/public/data`, {
headers: { 'X-API-Key': process.env.TEST_API_KEY || '' },
});
expect(response.status()).toBe(200);
});
});
Pagination Testing
test.describe('Pagination Tests', () => {
test('should return default page size when no limit specified', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.data.length).toBeLessThanOrEqual(20); // default limit
expect(body.meta.limit).toBe(20);
});
test('should paginate through all results correctly', async ({ request }) => {
const allItems: any[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { page: String(page), limit: '5' },
});
const body = await response.json();
allItems.push(...body.data);
hasMore = body.data.length === 5 && allItems.length < body.meta.total;
page++;
}
// Verify no duplicates across pages
const ids = allItems.map((item) => item.id);
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(ids.length);
});
test('should return empty array for page beyond total', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { page: '99999', limit: '10' },
});
expect(response.status()).toBe(200);
const body = await response.json();
expect(body.data).toEqual([]);
expect(body.meta.page).toBe(99999);
});
test('should reject invalid pagination parameters', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { page: '-1', limit: '0' },
});
expect(response.status()).toBe(400);
});
test('should enforce maximum page size', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { page: '1', limit: '10000' },
});
const body = await response.json();
// API should cap the limit or return 400
expect(body.data.length).toBeLessThanOrEqual(100);
});
test('should support cursor-based pagination', async ({ request }) => {
const firstPage = await request.get(`${BASE_URL}/events`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { limit: '5' },
});
const firstBody = await firstPage.json();
expect(firstBody).toHaveProperty('nextCursor');
if (firstBody.nextCursor) {
const secondPage = await request.get(`${BASE_URL}/events`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { limit: '5', cursor: firstBody.nextCursor },
});
const secondBody = await secondPage.json();
const firstIds = firstBody.data.map((i: any) => i.id);
const secondIds = secondBody.data.map((i: any) => i.id);
const overlap = firstIds.filter((id: string) => secondIds.includes(id));
expect(overlap).toHaveLength(0);
}
});
});
Filtering and Sorting Parameter Testing
test.describe('Filtering and Sorting Tests', () => {
test('should filter by exact field match', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { category: 'electronics' },
});
expect(response.status()).toBe(200);
const body = await response.json();
body.data.forEach((product: any) => {
expect(product.category).toBe('electronics');
});
});
test('should filter by date range', async ({ request }) => {
const response = await request.get(`${BASE_URL}/orders`, {
headers: { Authorization: `Bearer ${authToken}` },
params: {
createdAfter: '2025-01-01',
createdBefore: '2025-12-31',
},
});
expect(response.status()).toBe(200);
const body = await response.json();
body.data.forEach((order: any) => {
const createdAt = new Date(order.createdAt);
expect(createdAt.getFullYear()).toBe(2025);
});
});
test('should sort by field ascending', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { sortBy: 'price', order: 'asc' },
});
expect(response.status()).toBe(200);
const body = await response.json();
for (let i = 1; i < body.data.length; i++) {
expect(body.data[i].price).toBeGreaterThanOrEqual(body.data[i - 1].price);
}
});
test('should sort by field descending', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { sortBy: 'createdAt', order: 'desc' },
});
expect(response.status()).toBe(200);
const body = await response.json();
for (let i = 1; i < body.data.length; i++) {
const current = new Date(body.data[i].createdAt).getTime();
const previous = new Date(body.data[i - 1].createdAt).getTime();
expect(current).toBeLessThanOrEqual(previous);
}
});
test('should handle search/text filter', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: { search: 'laptop' },
});
expect(response.status()).toBe(200);
const body = await response.json();
body.data.forEach((product: any) => {
const matchesName = product.name.toLowerCase().includes('laptop');
const matchesDesc = product.description.toLowerCase().includes('laptop');
expect(matchesName || matchesDesc).toBe(true);
});
});
test('should combine multiple filters', async ({ request }) => {
const response = await request.get(`${BASE_URL}/products`, {
headers: { Authorization: `Bearer ${authToken}` },
params: {
category: 'electronics',
minPrice: '100',
maxPrice: '500',
sortBy: 'price',
order: 'asc',
},
});
expect(response.status()).toBe(200);
const body = await response.json();
body.data.forEach((product: any) => {
expect(product.category).toBe('electronics');
expect(product.price).toBeGreaterThanOrEqual(100);
expect(product.price).toBeLessThanOrEqual(500);
});
});
});
Error Response Validation
test.describe('Error Response Validation', () => {
test('400 Bad Request - invalid request body', async ({ request }) => {
const response = await request.post(`${BASE_URL}/users`, {
headers: { Authorization: `Bearer ${authToken}` },
data: { email: 'not-an-email', name: '' },
});
expect(response.status()).toBe(400);
const body = await response.json();
expect(body).toHaveProperty('error');
expect(body).toHaveProperty('details');
expect(Array.isArray(body.details)).toBe(true);
body.details.forEach((detail: any) => {
expect(detail).toHaveProperty('field');
expect(detail).toHaveProperty('message');
});
});
test('401 Unauthorized - missing credentials', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users`);
expect(response.status()).toBe(401);
const body = await response.json();
expect(body.error).toBeDefined();
expect(response.headers()['www-authenticate']).toBeDefined();
});
test('403 Forbidden - insufficient permissions', async ({ request }) => {
const response = await request.delete(`${BASE_URL}/admin/settings`, {
headers: { Authorization: `Bearer ${regularUserToken}` },
});
expect(response.status()).toBe(403);
});
test('404 Not Found - nonexistent resource', async ({ request }) => {
const response = await request.get(`${BASE_URL}/users/nonexistent-id-12345`, {
headers: { Authorization: `Bearer ${authToken}` },
});
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 224
- Forks
- 27
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
api-test-suite-generator- Source
- github.com/pramoddutta/qaskills