testing-best-practices

v2026.09.24

Choose and write the right test type (Playwright > integration > unit) against isolated Neon branches. Use when adding, running, or debugging tests for a feature.

GitHub
Install command
npx skhub add andrelandgraf/testing-best-practices
Markdown
SKILL.md

Testing Best Practices

Choose the right test type, isolate data per suite, and run against a disposable Neon branch.

Prerequisites

Complete these setup recipes first:

  • Browser Tests with Playwright
  • Integration Tests
  • Unit Tests with Bun

Choosing a Test Type

Ask "how would a user verify this works?" and pick the highest applicable tier:

  1. Playwright (default) — UI interactions, visual feedback, form validation, multi-step flows, protected routes, accessibility.
  2. Integration — API responses (status, JSON shape), DB state after operations, server logic without UI, or when Playwright is too slow/complex.
  3. Unit — pure functions with complex logic, many edge cases, or type narrowing/assertions with no external dependencies.

Running

All tests run against an isolated, schema-only Neon branch that auto-deletes after 1 hour.

bun run test              # all tests
bun run test:playwright   # browser only
bun run test:integration  # integration only
bun run test:unit         # unit only

Folder Structure

Unit tests are co-located; Playwright and integration tests live under tests/.

src/lib/<domain>/<file>.test.ts   # unit (co-located)
tests/integration/<feature>.test.ts
tests/playwright/<feature>.spec.ts
tests/playwright/lib/                # Playwright helpers

Writing Tests

Playwright spec:

import { test, expect } from "@playwright/test";

test.describe("Feature Name", () => {
  test("should do expected behavior", async ({ page }) => {
    await page.goto("/feature");
  });
});

Integration — import the route handler directly instead of going over HTTP:

import { describe, it, expect } from "bun:test";
import { GET } from "@/app/api/feature/route";

describe("GET /api/feature", () => {
  it("returns expected response", async () => {
    const response = await GET();
    expect(response.status).toBe(200);
    const data = await response.json();
    expect(data.value).toBeDefined();
  });
});

Unit (co-located):

import { describe, it, expect } from "bun:test";
import { myFunction } from "./my-file";

describe("myFunction", () => {
  it("returns expected value", () => {
    expect(myFunction()).toBe("expected");
  });
});

Test Data Isolation

Tests run in parallel against the shared branch, so each suite must own its data. Generate unique users per spec (e.g. auth-test-${uuid}@example.com), avoid shared resources, and rely on the branch TTL for cleanup — never assume data from another test exists.

const testUser = await createTestUser({
  email: `auth-test-${uuid}@example.com`,
});

Common Patterns

// Protected route (Playwright)
test("redirects unauthenticated user", async ({ page }) => {
  await page.goto("/protected-page");
  await expect(page).toHaveURL(/sign-in/);
});

// Error state (Playwright)
test("shows error for invalid input", async ({ page }) => {
  await page.goto("/form");
  await page.getByRole("button", { name: /submit/i }).click();
  await expect(page.getByText(/error|required/i)).toBeVisible({
    timeout: 5000,
  });
});

Debugging

bunx playwright test --headed     # watch the browser
bunx playwright test --debug      # step through
bunx playwright show-report       # HTML report

bun test --only "test name"       # run a single test
bun test --watch                  # re-run on change

Failed Playwright runs save screenshots and traces to test-results/ — check there when CI fails.

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

Not specified

Source path

skills/testing-best-practices

Default branch

main

Latest commit

4449331

Tree SHA

1f78782