Skip to main content

API Testing Guide

Comprehensive guide to testing HTTP APIs with @esimplicity/stack-tests.

Overview

API testing validates your HTTP endpoints without browser overhead. Use @api tagged steps for fast, reliable backend testing.

Basic Requests

GET Requests

@api
Scenario: Fetch a resource
When I GET "/users/1"
Then the response status should be 200
And the response should be a JSON object

POST Requests

@api
Scenario: Create a resource
When I POST "/users" with JSON body:
"""
{
"email": "new@example.com",
"name": "New User"
}
"""
Then the response status should be 201

PATCH Requests

@api
Scenario: Update a resource
When I PATCH "/users/1" with JSON body:
"""
{
"name": "Updated Name"
}
"""
Then the response status should be 200

PUT Requests

@api
Scenario: Replace a resource
When I PUT "/users/1" with JSON body:
"""
{
"email": "replaced@example.com",
"name": "Replaced User"
}
"""
Then the response status should be 200

DELETE Requests

@api
Scenario: Delete a resource
When I DELETE "/users/1"
Then the response status should be 204

Authentication

Admin Authentication

@api
Scenario: Admin operation
Given I am authenticated as an admin via API
When I POST "/admin/users" with JSON body:
"""
{ "email": "admin-created@example.com" }
"""
Then the response status should be 201

User Authentication

@api
Scenario: User operation
Given I am authenticated as a user via API
When I GET "/profile"
Then the response status should be 200

Custom Token

@api
Scenario: Use specific token
Given I set variable "token" to "eyJhbG..."
Given I set bearer token from variable "token"
When I GET "/protected"
Then the response status should be 200

Custom Headers

@api
Scenario: Custom authentication
Given I set header "X-API-Key" to "my-api-key"
When I GET "/api-key-protected"
Then the response status should be 200

Response Assertions

Status Codes

Then the response status should be 200
Then the response status should be 201
Then the response status should be 204
Then the response status should be 400
Then the response status should be 404

Response Type

Then the response should be a JSON array
Then the response should be a JSON object

Value Assertions

# Direct value
Then the value at "email" should equal "test@example.com"

# Nested path
Then the value at "user.profile.name" should equal "John"

# Array access
Then the value at "items[0].id" should equal "1"
Then the value at "data[0].attributes.title" should equal "First Item"

Variable Comparison

Given I set variable "expectedEmail" to "test@example.com"
Then the value at "email" should equal "{expectedEmail}"

Variable Management

Storing Response Values

@api
Scenario: Store and reuse values
When I POST "/users" with JSON body:
"""
{ "email": "test@example.com" }
"""
Then the response status should be 201
And I store the value at "id" as "userId"
And I store the value at "email" as "userEmail"

When I GET "/users/{userId}"
Then the response status should be 200
And the value at "email" should equal "{userEmail}"

Generated Data

@api
Scenario: Unique test data
Given I generate a UUID and store as "uniqueId"
Given I set variable "email" to "user-{uniqueId}@test.com"

When I POST "/users" with JSON body:
"""
{ "email": "{email}" }
"""
Then the response status should be 201

Path Variables

@api
Scenario: Dynamic paths
Given I set variable "teamId" to "team-123"
Given I set variable "memberId" to "member-456"

When I GET "/teams/{teamId}/members/{memberId}"
Then the response status should be 200

Cleanup

Manual Cleanup Registration

@api
Scenario: Create with cleanup
Given I am authenticated as an admin via API
When I POST "/users" with JSON body:
"""
{ "email": "temp@example.com" }
"""
Then I store the value at "id" as "userId"
Given I register cleanup DELETE "/users/{userId}"

Automatic Cleanup

The DefaultCleanupAdapter auto-registers cleanup based on variable names:

Variable PatternCleanup Path
userId, user*/admin/users/{id}
teamId, team*/admin/teams/{id}
workspaceId/admin/workspaces/{id}

Skip Cleanup (Debugging)

@api
Scenario: Debug without cleanup
Given I disable cleanup
When I POST "/users" with JSON body: ...
# Resource remains after test for inspection

Testing Patterns

CRUD Operations

@api
Feature: User CRUD

Background:
Given I am authenticated as an admin via API

Scenario: Create, Read, Update, Delete
# Create
When I POST "/users" with JSON body:
"""
{ "email": "crud@example.com", "name": "CRUD User" }
"""
Then the response status should be 201
And I store the value at "id" as "userId"

# Read
When I GET "/users/{userId}"
Then the response status should be 200
And the value at "email" should equal "crud@example.com"

# Update
When I PATCH "/users/{userId}" with JSON body:
"""
{ "name": "Updated User" }
"""
Then the response status should be 200
And the value at "name" should equal "Updated User"

# Delete
When I DELETE "/users/{userId}"
Then the response status should be 204

List and Filter

@api
Scenario: List with filters
When I GET "/users?role=admin&status=active"
Then the response status should be 200
And the response should be a JSON array

Pagination

@api
Scenario: Paginated results
When I GET "/users?page=1&limit=10"
Then the response status should be 200
And the value at "meta.page" should equal "1"
And the value at "meta.limit" should equal "10"

Error Handling

@api
Scenario: Handle not found
When I GET "/users/nonexistent"
Then the response status should be 404

@api
Scenario: Handle validation error
When I POST "/users" with JSON body:
"""
{ "email": "invalid-email" }
"""
Then the response status should be 400

Scenario Outline

@api
Scenario Outline: Create users with different roles
Given I am authenticated as an admin via API
When I POST "/users" with JSON body:
"""
{ "email": "<email>", "role": "<role>" }
"""
Then the response status should be 201
And the value at "role" should equal "<role>"

Examples:
| email | role |
| admin@test.com | admin |
| member@test.com | member |
| guest@test.com | guest |

Environment Configuration

Required Variables

# .env
API_BASE_URL=http://localhost:3000
API_AUTH_LOGIN_PATH=/auth/login

DEFAULT_ADMIN_USERNAME=admin@example.com
DEFAULT_ADMIN_PASSWORD=admin123

DEFAULT_USER_USERNAME=user@example.com
DEFAULT_USER_PASSWORD=user123

Note: All credentials must be explicitly set. If env vars are missing, auth methods skip silently with a console.warn.

Multiple Environments

# .env.staging
API_BASE_URL=https://api.staging.example.com

# .env.production
API_BASE_URL=https://api.example.com
# Run with specific env
ENV_FILE=.env.staging npm test

Best Practices

Use Backgrounds for Setup

@api
Feature: User API

Background:
Given I am authenticated as an admin via API
# Runs before each scenario

Isolate Test Data

@api
Scenario: Isolated data
Given I generate a UUID and store as "testId"
Given I set variable "email" to "test-{testId}@example.com"
# Unique data prevents conflicts

Keep Scenarios Independent

# Good - self-contained
@api
Scenario: Create and verify user
Given I am authenticated as admin
When I POST "/users" ...
Then I store the value at "id" as "userId"
When I GET "/users/{userId}"
Then the response status should be 200

# Avoid - depends on other scenarios
@api
Scenario: Verify user from previous test
When I GET "/users/{userId}" # Where did userId come from?

Meaningful Variable Names

# Good
Then I store the value at "id" as "createdUserId"
Then I store the value at "token" as "adminAccessToken"

# Avoid
Then I store the value at "id" as "id"
Then I store the value at "token" as "t"

Debugging

Inspect Response

Add custom steps to log responses:

Then('I log the response', async ({ world }) => {
console.log('Status:', world.lastStatus);
console.log('Body:', JSON.stringify(world.lastJson, null, 2));
});

Debug Mode

# Run with debug output
DEBUG=pw:api npm test