Skip to content

INTEGRATION TESTING · LESSON 12 OF 24

API tests and owned test data

Test HTTP behavior with a separate data namespace for each scenario.

  • request fixture
  • status codes
  • JSON
  • cleanup

Your runnable checkpoint

Extract into a new practice folder. Each download contains tests, configuration, the complete local practice app and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.

Install dependencies and browser binaries using the commands below. The test runner starts the practice server on port 4174. Tests target localhost, never the production EduPoly site.

1. Understand the concepts

The request fixture makes HTTP requests without a browser page. Use it to test API contracts or create owned data efficiently. A successful request still needs assertions about status, response shape and meaningful behavior.

The practice server has an in-memory task API only on localhost. Every request must include X-Test-Id, and each namespace has its own records. This is a test-isolation mechanism, not an authentication design.

Create a random scope for each test. Delete only that scope in finally so a failure does not leave data behind. A global reset would let parallel tests erase one another’s records.

The API data and the browser’s local task list are intentionally separate in this lab. API tests do not automatically seed the UI. When integrating a real application, verify that the UI actually reads the backend you are preparing.

2. Build and run

These are complete test files and configuration. The download also includes app/ and server.mjs; preserve their paths. Stop any manually started practice server before running the suite.

Terminal
npm install
npx playwright install chromium
npm run check
npm test

tests/lesson.spec.ts

tests/lesson.spec.ts
import { test, expect } from '@playwright/test';
import { randomUUID } from 'node:crypto';
test('create, read and remove an API task', async ({ request }) => {
  const headers = { 'X-Test-Id': randomUUID() };
  try {
    const created = await request.post('/api/tasks', { headers, data: { title: 'API task' } });
    expect(created.status()).toBe(201);
    const task = await created.json();
    expect(task).toMatchObject({ title: 'API task', done: false });
    expect(typeof task.id).toBe('string');
    const read = await request.get(`/api/tasks/${task.id}`, { headers });
    expect(read.status()).toBe(200);
    const removed = await request.delete(`/api/tasks/${task.id}`, { headers });
    expect(removed.status()).toBe(204);
    expect((await request.get(`/api/tasks/${task.id}`, { headers })).status()).toBe(404);
  } finally { await request.delete('/api/tasks', { headers }); }
});

playwright.config.ts

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 2 : undefined,
  timeout: 30000,
  expect: { timeout: 5000 },
  reporter: [['list'], ['html', { open: 'never' }]],
  use: { baseURL: 'http://127.0.0.1:4174', trace: 'retain-on-failure', screenshot: 'only-on-failure' },
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
  webServer: { command: 'node server.mjs', url: 'http://127.0.0.1:4174', reuseExistingServer: false, timeout: 15000 }
});

package.json

package.json
{
  "name": "edupoly-playwright-api-tests-and-owned-data",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "test": "playwright test",
    "test:headed": "playwright test --headed",
    "test:ui": "playwright test --ui",
    "check": "tsc --noEmit",
    "report": "playwright show-report",
    "serve": "node server.mjs"
  },
  "devDependencies": {
    "@playwright/test": "1.56.1",
    "typescript": "5.9.3",
    "@types/node": "22.20.5"
  }
}

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "types": [
      "node"
    ],
    "noEmit": true
  },
  "include": [
    "tests/**/*.ts",
    "pages/**/*.ts",
    "playwright.config.ts"
  ]
}

.gitignore

.gitignore
node_modules/
playwright-report/
test-results/
playwright/.auth/
.env

3. Trace the code and check the result

  1. randomUUID keeps data ownership separate across workers.
  2. Status and body assertions check different parts of the contract.
  3. finally runs even when an assertion fails.

Verify your result

  • Create returns 201; delete returns 204; reading the deleted id returns 404.
  • Cleanup affects only this test’s namespace.

4. Practice independently

Add a negative test for a blank API title.

Show the explanation

Use a new X-Test-Id and POST { title: " " }. Expect 422, then clean up that namespace.

When something goes wrong

If a browser executable is missing, run npx playwright install chromium in the project folder. If port 4174 is occupied, stop your manual practice server. Read assertion call logs for mismatched locators or expectations. Use npm run report to inspect failures; do not add fixed sleeps or broad resets to hide them.

Read the related official Playwright documentation ↗