Writing Test Cases
Learn patterns for writing effective external test cases.
Basic Structure
Use the testCase() wrapper to define test cases:
import { testCase } from '@reapi/test-sdk'
export const myTest = testCase(
{
id: 'feature-test-name', // Unique, kebab-case
name: 'Feature Test Name', // Display name
description: 'What this validates',
tags: ['feature', 'smoke'], // For filtering
priority: 1, // Lower = runs first
timeout: 30000, // Max execution time
},
async () => {
// Test implementation
}
)Making API Requests
Use fetch or ky for HTTP requests:
function: async () => {
// Using fetch
const response = await fetch(`${$server.baseUrl}/api/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...$auth.headers, // Includes auth headers
},
body: JSON.stringify({
email: faker.internet.email(),
name: faker.person.fullName(),
}),
});
const data = await response.json();
// Using ky (built-in)
const data2 = await ky.post(`${$server.baseUrl}/api/users`, {
json: { email: faker.internet.email() },
headers: $auth.headers,
}).json();
}Recording Assertions
Always use $addAssertionResult():
function: async () => {
const response = await fetch(`${$server.baseUrl}/api/users`);
const users = await response.json();
// Assertion 1: Status code
$addAssertionResult({
passed: response.status === 200,
message: `Status: ${response.status}`,
operator: "equals",
leftValue: response.status,
rightValue: 200,
});
// Assertion 2: Response structure
$addAssertionResult({
passed: Array.isArray(users.data),
message: Array.isArray(users.data)
? `Got ${users.data.length} users`
: "Response is not an array",
operator: "isArray",
leftValue: typeof users.data,
rightValue: "array",
});
// Assertion 3: Data validation with Zod
const schema = z.array(z.object({
id: z.number(),
email: z.string().email(),
}));
const validation = schema.safeParse(users.data);
$addAssertionResult({
passed: validation.success,
message: validation.success
? "Users match schema"
: `Schema error: ${validation.error.message}`,
operator: "matchesSchema",
leftValue: users.data,
rightValue: "user schema",
});
}Multi-Step Workflows
Chain multiple API calls:
import { testCase } from '@reapi/test-sdk'
export const userWorkflowTest = testCase(
{
id: 'user-crud-workflow',
name: 'User CRUD Workflow',
tags: ['users', 'crud', 'regression'],
},
async () => {
const baseUrl = $server.baseUrl
const headers = {
'Content-Type': 'application/json',
...$auth.headers,
}
// Step 1: Create user
const createRes = await fetch(`${baseUrl}/api/users`, {
method: 'POST',
headers,
body: JSON.stringify({
email: faker.internet.email(),
name: faker.person.fullName(),
}),
})
const created = await createRes.json()
$addAssertionResult({
passed: createRes.status === 201,
message: `Create user: ${createRes.status}`,
operator: 'create',
leftValue: createRes.status,
rightValue: 201,
})
if (createRes.status !== 201) return // Stop if failed
const userId = created.data.id
// Step 2: Read user
const getRes = await fetch(`${baseUrl}/api/users/${userId}`, { headers })
const fetched = await getRes.json()
$addAssertionResult({
passed: getRes.status === 200 && fetched.data.id === userId,
message: `Read user: ${getRes.status}`,
operator: 'read',
leftValue: fetched.data?.id,
rightValue: userId,
})
// Step 3: Update user
const updateRes = await fetch(`${baseUrl}/api/users/${userId}`, {
method: 'PATCH',
headers,
body: JSON.stringify({ name: 'Updated Name' }),
})
$addAssertionResult({
passed: updateRes.status === 200,
message: `Update user: ${updateRes.status}`,
operator: 'update',
leftValue: updateRes.status,
rightValue: 200,
})
// Step 4: Delete user
const deleteRes = await fetch(`${baseUrl}/api/users/${userId}`, {
method: 'DELETE',
headers,
})
$addAssertionResult({
passed: deleteRes.status === 204,
message: `Delete user: ${deleteRes.status}`,
operator: 'delete',
leftValue: deleteRes.status,
rightValue: 204,
})
}
)Using Context
Share data between tests:
import { testCase } from '@reapi/test-sdk'
// Test 1: Create and store
export const createOrderTest = testCase(
{
id: 'order-create',
name: 'Create Order',
tags: ['orders', 'setup'],
priority: 1,
},
async () => {
const response = await ky.post(`${$server.baseUrl}/api/orders`, {
json: { items: [{ productId: 1, quantity: 2 }] },
headers: $auth.headers,
}).json()
// Store for later tests
$context.orderId = response.data.id
$context.orderTotal = response.data.total
$addAssertionResult({
passed: !!response.data.id,
message: 'Order created',
operator: 'created',
leftValue: response.data.id,
rightValue: 'exists',
})
}
)
// Test 2: Use stored data
export const verifyOrderTest = testCase(
{
id: 'order-verify',
name: 'Verify Order',
tags: ['orders', 'verification'],
priority: 2,
},
async () => {
const orderId = $context.orderId // From previous test
const response = await ky.get(
`${$server.baseUrl}/api/orders/${orderId}`,
{ headers: $auth.headers }
).json()
$addAssertionResult({
passed: response.data.total === $context.orderTotal,
message: 'Order total matches',
operator: 'equals',
leftValue: response.data.total,
rightValue: $context.orderTotal,
})
}
)Error Handling
Handle failures gracefully:
function: async () => {
try {
const response = await fetch(`${$server.baseUrl}/api/flaky-endpoint`);
const data = await response.json();
$addAssertionResult({
passed: response.ok,
message: response.ok ? "Request succeeded" : `Failed: ${response.status}`,
operator: "request",
leftValue: response.status,
rightValue: "2xx",
});
} catch (error) {
$addAssertionResult({
passed: false,
message: `Request failed: ${error.message}`,
operator: "request",
leftValue: error.message,
rightValue: "success",
});
}
}Logging
Use $log() for debugging:
function: async () => {
$log("info", "Starting test", { server: $server.baseUrl });
const response = await fetch(`${$server.baseUrl}/api/users`);
const data = await response.json();
$log("debug", "Response received", {
status: response.status,
count: data.data?.length,
});
if (response.status !== 200) {
$log("error", "Unexpected status", { status: response.status, body: data });
}
}Reporting API Calls
Track API calls in test reports:
function: async () => {
const startTime = Date.now();
const response = await fetch(`${$server.baseUrl}/api/users`);
const duration = Date.now() - startTime;
// Report the API call
$reportApiRequest({
method: "GET",
url: `${$server.baseUrl}/api/users`,
status: response.status,
duration,
requestHeaders: { ...$auth.headers },
responseBody: await response.clone().json(),
});
}Tagging Strategy
Use consistent tags for filtering:
// By feature
tags: ["auth", "login"]
tags: ["users", "crud"]
tags: ["payments", "checkout"]
// By test type
tags: ["smoke"] // Critical path tests
tags: ["regression"] // Full regression suite
tags: ["e2e"] // End-to-end flows
// By priority
tags: ["critical"] // Must pass
tags: ["p1"] // High priority
tags: ["p2"] // Medium priority
// Combined
tags: ["auth", "smoke", "critical"]
tags: ["payments", "regression", "p1"]Best Practices
- One assertion per check - Makes reports clear
- Use descriptive messages - Help debugging
- Handle errors - Don’t let tests crash
- Use context for shared data - Not global variables
- Tag consistently - Enable flexible test selection
- Set appropriate timeouts - Avoid false failures