Prompt file imported from jordasvs/playwright-mcp-copilot-agent (
.github/prompts/generate_test_api.prompt.md). Fill in{{baseURL}},{{petId}},{{id}},{{body_name}},{{body_id}},{{body_status}},{{createdPetId}},{{error_message}},{{attempt}},{{delay}},{{maxRetries}},{{description}},{{duration}},{{statusCode}},{{config_apiKey}},{{status}},{{responseTime}},{{authToken}}before use. Copyright stays with the author.
š¤ API Testing with Playwright - CURL-Powered Guide
š Instructions for API Test Generation from CURL Commands
When you provide a CURL command for an API endpoint, I'll help you create a simple, effective API test using Playwright. This approach makes it easy to convert your existing API calls directly into automated tests.
How to Provide CURL Commands:
Simply provide the CURL command for the API you want to test. For example:
curl -X 'GET' \
'https://petstore.swagger.io/v2/pet/20' \
-H 'accept: application/json'
Additional Information to Consider:
-
API Context (if not clear from CURL):
- Swagger/OpenAPI URL (if available)
- API version context (if needed)
-
Test Scenarios:
- Edge cases to test (e.g., invalid IDs, missing parameters)
- Additional operations (create, update, delete) if you want a full CRUD suite
-
Expected Response:
- Expected status code if not standard (200 for GET, 201 for POST, etc.)
- Critical fields that should be validated
-
Authentication Context:
- Token lifecycle information if the CURL contains auth tokens
- How to refresh/obtain tokens if needed
Example Format with CURL:
CURL:
curl -X 'GET' \
'https://petstore.swagger.io/v2/pet/20' \
-H 'accept: application/json'
Additional Context:
- Swagger URL: https://petstore.swagger.io/
- This endpoint retrieves pet details by ID
Expected Response:
- Status: 200
- Body should contain fields: id, name, status
- Example response structure: {
"id": 20,
"category": {"id": 204, "name": "Shiro"},
"name": "doggie",
"photoUrls": ["string"],
"tags": [{"id": 333, "name": "preethi"}],
"status": "sold"
}
Additional Test Cases:
- Test with non-existent pet ID (expect 404)
šļø Test Structure & CURL Conversion
I'll organize your test files with this minimal, effective structure:
api-tests/
āāā data/
ā āāā [resource]-data.json # Test data for requests and assertions
āāā helpers/
ā āāā api-helpers.js # Optional: Reusable functions
āāā specs/
āāā [resource].spec.js # Test implementation
CURL to Playwright Test Conversion
Converting CURL commands to Playwright tests is straightforward:
| CURL Component | Playwright Equivalent |
|---|---|
-X 'GET' |
request.get(url) |
-X 'POST' |
request.post(url, { data: body }) |
-X 'PUT' |
request.put(url, { data: body }) |
-X 'DELETE' |
request.delete(url) |
-H 'header: value' |
Include in headers object |
-d '{"key":"value"}' |
Include in data object |
Example: Converting a CURL command to a Playwright test
# Original CURL
curl -X 'GET' \
'https://petstore.swagger.io/v2/pet/20' \
-H 'accept: application/json'
# Converted Playwright Test
test('Should get pet by ID', async ({ request }) => {
// Arrange
const petId = 20;
const baseURL = 'https://petstore.swagger.io/v2';
// Act
const response = await request.get(`{{baseURL}}/pet/{{petId}}`, {
headers: {
'accept': 'application/json'
}
});
const body = await response.json();
// Assert
expect(response.status()).toBe(200);
expect(body).toHaveProperty('id', petId);
});
File Organization Best Practices
ā Keep related tests in a single file: All tests related to the same resource (e.g., pets, users, orders) should be kept in a single spec file, even if they involve different HTTP methods (GET, POST, PUT, DELETE). This improves:
- Maintainability: All related functionality is in one place
- Test flow: Makes it easier to follow the logical flow of operations
- Shared state: Enables easy sharing of test data between related tests
- Clarity: Provides a complete picture of how the API resource works
For example, put all pet-related operations (get, create, update, delete) in a single pet.spec.js file instead of separating them into multiple files.
š Test Implementation Approach
Each test will follow the AAA (Arrange-Act-Assert) pattern:
// Arrange: Set up test data and prerequisites
const petId = 20;
const expectedStatus = 200;
// Act: Make the API request
const response = await request.get(`{{baseURL}}/pet/{{petId}}`);
const body = await response.json();
// Assert: Validate the response
expect(response.status()).toBe(expectedStatus);
expect(body).toHaveProperty('id', petId);
expect(body).toHaveProperty('name');
š§ Handling Common API Testing Challenges
1. Managing Dependent Tests and Organizing Multiple Test Types
Here's how to organize multiple test types in a single file, including both independent and dependent tests:
// File: pet.spec.js
const { test, expect } = require('@playwright/test');
const { validateStatusCode, validateResponseFields } = require('../helpers/api-helpers');
const petData = require('../data/pet-data.json');
// Base URL for all requests
const baseURL = 'https://api.example.com/v2';
// Independent tests can run in parallel
test.describe('Pet API - Read Operations', () => {
test('Should get a pet by ID', async ({ request }) => {
// Arrange: Set up test data and prerequisites
const { id, expectedStatus, expectedFields } = petData.read.validPet;
// Act: Make the API request
const response = await request.get(`{{baseURL}}/pet/{{id}}`);
const body = await response.json();
// Assert: Validate the response
validateStatusCode(response, expectedStatus);
validateResponseFields(body, expectedFields);
expect(body.id).toBe(id);
// Log the pet details for debugging purposes
console.log(`Found pet: {{body_name}} (ID: {{body_id}}, Status: {{body_status}})`);
});
test('Should return 404 when getting non-existent pet', async ({ request }) => {
// Arrange: Set up test data for a non-existent pet
const { id, expectedStatus, expectedErrorMessage } = petData.read.invalidPet;
// Act: Make the API request
const response = await request.get(`{{baseURL}}/pet/{{id}}`);
// Assert: Validate the error response
validateStatusCode(response, expectedStatus);
// If the response has a body with an error message, validate it
if (response.status() !== 404) {
const body = await response.json();
expect(body.message).toBe(expectedErrorMessage);
}
});
});
// Dependent tests must run serially
test.describe.serial('Pet API - CRUD Operations', () => {
// Shared variable to pass state between tests
let createdPetId;
test('Should create a new pet', async ({ request }) => {
// Arrange: Set up test data for creating a new pet
const { name, category, photoUrls, tags, status, expectedStatus } = petData.crud.createPet;
// Prepare the pet data
const newPet = {
name,
category,
photoUrls,
tags,
status
};
// Act: Send the POST request to create a new pet
const response = await request.post(`{{baseURL}}/pet`, {
data: newPet,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
const body = await response.json();
// Save the created pet ID for use in subsequent tests
createdPetId = body.id;
// Assert: Validate the response
validateStatusCode(response, expectedStatus);
expect(body).toHaveProperty('id');
expect(body.name).toBe(name);
expect(body.status).toBe(status);
console.log(`Created new pet: {{body_name}} (ID: {{body_id}}, Status: {{body_status}})`);
});
test('Should update the pet status', async ({ request }) => {
// Ensure we have a created pet ID
expect(createdPetId).toBeTruthy();
// Arrange: Set up test data for updating the pet
const { status, expectedStatus } = petData.crud.updatePet;
// First, get the current pet data
const getResponse = await request.get(`{{baseURL}}/pet/{{createdPetId}}`);
const currentPet = await getResponse.json();
// Prepare updated pet data (only changing status)
const updatedPet = {
...currentPet,
status
};
// Act: Send the PUT request to update the pet
const response = await request.put(`{{baseURL}}/pet`, {
data: updatedPet,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
const body = await response.json();
// Assert: Validate the response
validateStatusCode(response, expectedStatus);
expect(body).toHaveProperty('id', createdPetId);
expect(body.status).toBe(status);
console.log(`Updated pet {{body_name}} (ID: {{body_id}}) status to: {{body_status}}`);
});
test('Should delete the pet', async ({ request }) => {
// Ensure we have a created pet ID
expect(createdPetId).toBeTruthy();
// Arrange: Get the expected status code for deletion
const { expectedStatus } = petData.crud.deletePet;
try {
// Act: Send the DELETE request
const response = await request.delete(`{{baseURL}}/pet/{{createdPetId}}`, {
headers: {
'Accept': 'application/json'
}
});
validateStatusCode(response, expectedStatus);
console.log(`Successfully deleted pet with ID: {{createdPetId}}`);
} catch (error) {
console.log(`Error during delete operation: {{error_message}}`);
}
// Verify the deletion
const getResponse = await request.get(`{{baseURL}}/pet/{{createdPetId}}`);
expect(getResponse.status()).toBe(404);
console.log(`Get after delete returned status: ${getResponse.status()}`);
});
});
Data Structure for Dependent Tests
The example above uses a structured data file that separates different test scenarios:
{
"read": {
"validPet": {
"id": 20,
"expectedStatus": 200,
"expectedFields": ["id", "name", "status"]
},
"invalidPet": {
"id": 999999999,
"expectedStatus": 404,
"expectedErrorMessage": "Pet not found"
}
},
"crud": {
"createPet": {
"name": "fluffy",
"category": {
"id": 1,
"name": "Dogs"
},
"photoUrls": [
"https://example.com/fluffy.jpg"
],
"tags": [
{
"id": 1,
"name": "friendly"
}
],
"status": "available",
"expectedStatus": 200
},
"updatePet": {
"status": "sold",
"expectedStatus": 200
},
"deletePet": {
"expectedStatus": 200
}
}
}
Key Practices for Managing Test Dependencies
- Use
test.describe.serialfor tests that must run in sequence - Share state using scope variables declared outside individual tests
- Perform explicit checks before using shared data in subsequent tests
- Use clear section comments (Arrange, Act, Assert) to maintain readability
- Keep test data organized in structured JSON by operation type
- Implement appropriate cleanup even if tests fail midway
- Log critical information about test progress and state changes
- Verify effects of operations independently of their direct responses
- Handle errors gracefully especially for operations known to be inconsistent
2. Robust Error Handling
API testing requires comprehensive error handling strategies to deal with various failure scenarios, including network issues, inconsistent API behavior, and service unavailability.
Basic Error Handling Pattern
// For operations that might behave inconsistently (like DELETE)
try {
const response = await request.delete(`{{baseURL}}/resource/{{id}}`);
expect(response.status()).toBe(expectedStatus);
} catch (error) {
console.log(`Error during operation: {{error_message}}`);
// Additional verification to confirm operation effect
const verificationResponse = await request.get(`{{baseURL}}/resource/{{id}}`);
expect(verificationResponse.status()).toBe(404); // Verify resource was deleted despite error
}
Advanced Error Handling Strategies
1. Retry Mechanism for Flaky APIs
/**
* Retry an API request with exponential backoff
* @param {Function} requestFn - Async function that makes the request
* @param {Function} validateFn - Function to validate the response
* @param {Object} options - Retry options
* @returns {Promise<Object>} - The successful response
*/
async function retryRequest(requestFn, validateFn, options = {}) {
const {
maxRetries = 3,
initialDelay = 1000,
factor = 2,
statusCodesToRetry = [408, 429, 500, 502, 503, 504]
} = options;
let lastError;
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await requestFn();
// If response status indicates retry is needed
if (response.status() && statusCodesToRetry.includes(response.status())) {
lastError = new Error(`Received status code ${response.status()}`);
throw lastError;
}
// Validate response if validation function provided
if (validateFn && !validateFn(response)) {
lastError = new Error('Response validation failed');
throw lastError;
}
return response; // Success
} catch (error) {
lastError = error;
attempt++;
if (attempt < maxRetries) {
const delay = initialDelay * Math.pow(factor, attempt - 1);
console.log(`Attempt {{attempt}} failed: {{error_message}}. Retrying in {{delay}}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
console.error(`All {{maxRetries}} retry attempts failed`);
throw lastError;
}
2. Structured Error Handling & Reporting
/**
* Makes an API request with structured error handling
* @param {string} description - Description of the request for logging
* @param {Function} requestFn - The request function to execute
* @returns {Promise<Object>} - The response object
*/
async function makeAPIRequest(description, requestFn) {
try {
console.log(`Executing: {{description}}`);
const startTime = Date.now();
const response = await requestFn();
const duration = Date.now() - startTime;
console.log(`ā
{{description}} - Completed in {{duration}}ms`);
console.log(` Status: ${response.status()}`);
return response;
} catch (error) {
console.error(`ā {{description}} - Failed after ${Date.now() - startTime}ms`);
console.error(` Error: {{error_message}}`);
// Capture additional diagnostic information
const diagnostics = {
timestamp: new Date().toISOString(),
operation: description,
error: {
message: error.message,
stack: error.stack
}
};
// Log structured error information for easier debugging
console.error('Diagnostic Information:', JSON.stringify(diagnostics, null, 2));
throw error;
}
}
3. Error Response Analysis
/**
* Analyzes an error response to provide meaningful feedback
* @param {Object} response - The API response
* @returns {Object} - Structured error details
*/
async function analyzeErrorResponse(response) {
const statusCode = response.status();
const contentType = response.headers()['content-type'] || '';
let body;
let errorDetails = { statusCode };
try {
if (contentType.includes('application/json')) {
body = await response.json();
errorDetails.body = body;
// Extract common error patterns from different APIs
errorDetails.message =
body.message ||
body.error ||
body.errorMessage ||
(body.errors && body.errors[0]?.message) ||
`Request failed with status {{statusCode}}`;
errorDetails.code = body.code || body.errorCode;
errorDetails.details = body.details || body.data;
} else if (contentType.includes('text/')) {
const text = await response.text();
errorDetails.body = text;
errorDetails.message = text || `Request failed with status {{statusCode}}`;
} else {
errorDetails.message = `Request failed with status {{statusCode}}`;
}
} catch (error) {
errorDetails.parsingError = error.message;
}
return errorDetails;
}
4. Comprehensive API Testing Error Analysis
When API tests fail, follow this approach:
-
Error Identification:
- Capture the HTTP status code and response body
- Log request details (URL, headers, body) for context
- Check for common error patterns (rate limits, authentication issues)
-
Response Analysis:
- Compare actual response structure with expected schema
- Identify missing or unexpected fields
- Check for data type or format issues
-
Request Validation:
- Verify correct request format and content
- Check if authorization tokens are valid
- Validate the request path and parameters
-
Environmental Factors:
- Check if the API environment is available
- Verify test preconditions were met
- Check for dependent services status
-
Documentation:
## API Error Analysis - Endpoint: [endpoint path] - Method: [HTTP method] - Status: [received status code] - Expected: [expected status] - Error: [error message from response] - Root Cause: [identified cause after analysis] - Resolution: [steps taken to resolve]
Using these advanced error handling techniques ensures that API tests are:
- Robust: Can handle intermittent failures gracefully
- Self-healing: Attempts to recover from transient issues
- Informative: Provides clear diagnostic information
- Maintainable: Follows consistent error handling patterns
3. Helper Functions
Always import necessary dependencies in helper files:
// In api-helpers.js
const { expect } = require('@playwright/test');
function validateStatusCode(response, expectedStatus) {
expect(response.status()).toBe(expectedStatus);
}
module.exports = { validateStatusCode };
š Benefits of This Approach
- Simplicity: Minimal files and structure for quick implementation
- Readability: Clear test organization using AAA pattern
- Maintenance: Easy to update and extend
- Playwright Integration: Leverages Playwright's built-in request API
- Focused Tests: Each test targets a specific API behavior
- Robustness: Handles edge cases and API inconsistencies
š” Tips for Effective API Testing
- Test both happy paths and error scenarios
- Validate all critical response fields
- Use dynamic data when appropriate
- Include authorization/authentication testing
- Verify correct status codes for all responses
- Implement comprehensive error handling strategies:
- Use retry mechanisms for flaky APIs
- Add structured error reporting
- Implement response analysis for error conditions
- Document error patterns and resolutions
- Organize related tests by resource
- Use serial execution for dependent tests
- Always use explicit assertions for test dependencies
- Log important information for debugging with appropriate detail level
- Consolidate related tests in a single file, even when using different HTTP methods
- Use separate describe blocks within a file to organize different types of tests
- Implement timeout strategies appropriate for the API's expected response times
- Consider performance monitoring in your test assertions
- Handle eventual consistency in distributed systems by implementing appropriate waits
ļæ½ Advanced API Testing Techniques
1. Advanced Response Validation
Beyond basic field validation, implement comprehensive response validation:
/**
* Validates a JSON response against a schema
* @param {Object} body - Response body
* @param {Object} schema - Schema definition
*/
function validateSchema(body, schema) {
// This is a simplified example - consider using a proper schema validation library
for (const [key, requirements] of Object.entries(schema)) {
// Check property exists if required
if (requirements.required) {
expect(body).toHaveProperty(key);
}
// Skip further validation if property doesn't exist and is not required
if (!(key in body) && !requirements.required) {
continue;
}
// Validate type
if (requirements.type) {
const actualType = Array.isArray(body[key]) ? 'array' : typeof body[key];
expect(actualType).toBe(requirements.type);
}
// Validate array items if specified and property is an array
if (requirements.itemType && Array.isArray(body[key])) {
for (const item of body[key]) {
const itemType = typeof item;
expect(itemType).toBe(requirements.itemType);
}
}
// Validate nested objects
if (requirements.properties && typeof body[key] === 'object' && !Array.isArray(body[key])) {
validateSchema(body[key], requirements.properties);
}
// Validate value constraints
if ('enum' in requirements && requirements.enum.length) {
expect(requirements.enum).toContain(body[key]);
}
}
}
// Example usage:
const petSchema = {
id: { type: 'number', required: true },
name: { type: 'string', required: true },
status: { type: 'string', required: true, enum: ['available', 'pending', 'sold'] },
tags: {
type: 'array',
required: false,
itemType: 'object',
properties: {
id: { type: 'number', required: true },
name: { type: 'string', required: true }
}
}
};
validateSchema(petResponse, petSchema);
2. Environment Configuration
Configure API tests to run against different environments:
// api.config.js
const { defineConfig } = require('@playwright/test');
const { env } = require('process');
// Define environment-specific configurations
const environments = {
development: {
baseURL: 'https://dev-api.example.com/v2',
apiKey: 'dev-key',
timeout: 30000 // longer timeout for dev
},
staging: {
baseURL: 'https://staging-api.example.com/v2',
apiKey: 'staging-key',
timeout: 15000
},
production: {
baseURL: 'https://api.example.com/v2',
apiKey: env.API_KEY, // Use environment variable for sensitive data
timeout: 10000
}
};
// Select environment from command line or default to development
const environment = env.API_ENV || 'development';
const config = environments[environment];
module.exports = defineConfig({
testDir: './api-tests/specs',
timeout: config.timeout,
use: {
extraHTTPHeaders: {
'Authorization': `Bearer {{config_apiKey}}`,
'Content-Type': 'application/json'
},
baseURL: config.baseURL
},
projects: [
{
name: 'api',
testMatch: /.*\.spec\.js/,
}
]
});
3. Parameterized API Tests
Create data-driven tests that run the same test logic with different parameters:
const { test, expect } = require('@playwright/test');
const { validateStatusCode } = require('../helpers/api-helpers');
const testData = require('../data/pets-data.json');
test.describe('Get pet by status', () => {
// Define test cases for different statuses
const statuses = ['available', 'pending', 'sold'];
for (const status of statuses) {
test(`Should get pets with status "{{status}}"`, async ({ request }) => {
// Arrange
const expectedStatus = 200;
const url = `/pet/findByStatus?status={{status}}`;
// Act
const response = await request.get(url);
const body = await response.json();
// Assert
validateStatusCode(response, expectedStatus);
expect(Array.isArray(body)).toBeTruthy();
// If we got any results, verify they all have the correct status
if (body.length > 0) {
const allHaveCorrectStatus = body.every(pet => pet.status === status);
expect(allHaveCorrectStatus).toBeTruthy();
}
});
}
});
4. API Performance Testing
Add performance assertions to your API tests:
test('API should respond within acceptable time', async ({ request }) => {
// Arrange
const url = '/pet/findByStatus?status=available';
const maxAcceptableTime = 300; // milliseconds
// Act
const startTime = Date.now();
const response = await request.get(url);
const endTime = Date.now();
const responseTime = endTime - startTime;
// Assert
expect(response.status()).toBe(200);
expect(responseTime).toBeLessThan(maxAcceptableTime);
console.log(`Response time: {{responseTime}}ms`);
});
5. Handling Authentication and Sessions
Implement proper authentication handling:
// In your test file
let authToken;
test.beforeAll(async ({ request }) => {
// Get authentication token before running tests
const response = await request.post('/auth/login', {
data: {
username: process.env.API_USERNAME,
password: process.env.API_PASSWORD
}
});
const body = await response.json();
authToken = body.token;
expect(authToken).toBeTruthy();
});
test('Access protected resource with auth token', async ({ request }) => {
// Ensure we have a token
expect(authToken).toBeTruthy();
// Make request with authentication
const response = await request.get('/protected-resource', {
headers: {
'Authorization': `Bearer {{authToken}}`
}
});
expect(response.status()).toBe(200);
});
ļæ½ CURL Command Analysis & Conversion
Common CURL Patterns and Their Playwright Equivalents
1. Basic GET Request
# CURL
curl -X GET 'https://api.example.com/resources/123' -H 'Accept: application/json'
# Playwright
const response = await request.get('https://api.example.com/resources/123', {
headers: {
'Accept': 'application/json'
}
});
2. POST with JSON Body
# CURL
curl -X POST 'https://api.example.com/resources' \
-H 'Content-Type: application/json' \
-d '{"name": "example", "value": 123}'
# Playwright
const response = await request.post('https://api.example.com/resources', {
headers: {
'Content-Type': 'application/json'
},
data: {
name: 'example',
value: 123
}
});
3. Authorization Headers
# CURL
curl -X GET 'https://api.example.com/protected' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
# Playwright
const response = await request.get('https://api.example.com/protected', {
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
}
});
4. Query Parameters
# CURL
curl -X GET 'https://api.example.com/search?q=keyword&page=1&limit=10'
# Playwright
const response = await request.get('https://api.example.com/search', {
params: {
q: 'keyword',
page: 1,
limit: 10
}
});
5. Form Data
# CURL
curl -X POST 'https://api.example.com/form' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'field1=value1&field2=value2'
# Playwright
const response = await request.post('https://api.example.com/form', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
form: {
field1: 'value1',
field2: 'value2'
}
});
6. File Upload
# CURL
curl -X POST 'https://api.example.com/upload' \
-F 'file=@/path/to/file.jpg' \
-F 'description=Image upload'
# Playwright
const formData = new FormData();
formData.append('file', fs.createReadStream('/path/to/file.jpg'));
formData.append('description', 'Image upload');
const response = await request.post('https://api.example.com/upload', {
multipart: {
file: {
name: 'file.jpg',
mimeType: 'image/jpeg',
buffer: fs.readFileSync('/path/to/file.jpg')
},
description: 'Image upload'
}
});
ļæ½š Ready to Generate Tests from CURL Commands
When you provide a CURL command, I'll generate a complete API test that:
- Properly parses and extracts all components from your CURL command
- Converts the CURL command to Playwright API test format
- Makes the API request with the correct parameters and headers
- Validates the response structure and status code
- Implements all requested test scenarios and edge cases
- Uses best practices for API testing with Playwright
- Properly handles test dependencies and errors
- Implements robust error handling and retry strategies
- Provides detailed logging and diagnostics
- Uses parameterization where appropriate
Let's convert your CURL commands into effective, maintainable API tests!