Test Organization
File structure
Organize tests to mirror the source code:
tests/
setup.ts # Global setup (test database, env)
helpers/
factories.ts # createAuthor, createBook, createReview
request.ts # get, post, put, del, jsonRequest
auth.ts # createTestToken, authHeader
mocks.ts # createEmailMock, createPaymentMock
unit/
transformers.test.ts # formatBookV1, formatBookV2
schemas.test.ts # Zod schema validation
helpers.test.ts # slugify, paginate, etc.
integration/
books.test.ts # Book CRUD endpoints
reviews.test.ts # Review endpoints
auth.test.ts # Protected endpoints, login
errors.test.ts # Error response consistency
versioning.test.ts # v1 vs v2 responses
advanced/
edge-cases.test.ts # Boundary values, special chars
background-jobs.test.ts # Job enqueueing and handling Unit tests in unit/. Integration tests in integration/. Each test file corresponds to a feature or component.
Naming conventions
Test files: feature.test.ts. Match the source file: transformers.ts → transformers.test.ts.
Describe blocks: Name the function or endpoint being tested.
Test names: Describe the expected behavior, not the implementation.
// GOOD: describes behavior
test("returns 404 for nonexistent book");
test("trims whitespace from title before saving");
test("rejects rating below 1");
// BAD: describes implementation
test("calls db.prepare with SELECT");
test("runs trim() on the title string");
test("checks if rating < 1"); Running subsets
# Run all tests
npm test
# Run only unit tests
npx vitest run tests/unit
# Run only integration tests
npx vitest run tests/integration
# Run a specific test file
npx vitest run tests/integration/books.test.ts
# Run tests matching a pattern
npx vitest run -t "returns 404"
# Watch mode (re-runs on file changes)
npx vitest Running subsets is useful during development: work on the books endpoint → run only books.test.ts. Before committing → run all tests.
Test scripts in package.json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:unit": "vitest run tests/unit",
"test:integration": "vitest run tests/integration",
"test:coverage": "vitest run --coverage"
}
} CI integration
Tests should run on every push. A basic GitHub Actions workflow:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm test [!NOTE] The Deploying with Docker course set up CI/CD pipelines. Adding
npm testto the pipeline ensures tests run before every deploy — bugs are caught before they reach production.
How many tests is enough?
There is no magic number. A practical guideline:
Every endpoint should have at least: one happy path test (200/201), one validation failure test (400), one not found test (404 for detail endpoints).
Every business rule should have a test: “Users cannot review their own books.” “Admins can delete books.”
Every bug fix should come with a test that fails before the fix and passes after. This prevents the bug from returning.
Exercises
Exercise 1: Organize your tests into the file structure above. Run unit and integration tests separately.
Exercise 2: Add test scripts to package.json. Run each one.
Exercise 3: Add a CI configuration that runs tests on every push.
Why should test names describe behavior, not implementation?