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

  1. One assertion per check - Makes reports clear
  2. Use descriptive messages - Help debugging
  3. Handle errors - Don’t let tests crash
  4. Use context for shared data - Not global variables
  5. Tag consistently - Enable flexible test selection
  6. Set appropriate timeouts - Avoid false failures