--- title: Testing description: Run tests and check code quality. --- ## `hatk test` Run your project's test suite. ```bash hatk test # Run all tests hatk test --unit # Unit tests only hatk test --integration # Integration tests only hatk test --browser # Playwright browser tests ``` | Flag | Description | | --------------- | ------------------------------------------------ | | `--unit` | Run unit tests in `test/feeds/` and `test/xrpc/` | | `--integration` | Run integration tests in `test/integration/` | | `--browser` | Run Playwright browser tests in `test/browser/` | Without flags, all test types are run. ## Writing unit tests Unit tests use `createTestContext()` from `hatk/test` to boot an in-memory hatk — lexicons, SQLite, feeds, and XRPC handlers — with no HTTP server, no PDS, and no indexer. ```typescript import { describe, test, expect, beforeAll, afterAll } from 'vitest' import { createTestContext } from 'hatk/test' let ctx: Awaited> beforeAll(async () => { ctx = await createTestContext() await ctx.loadFixtures() }) afterAll(async () => ctx?.close()) ``` ### Test context API | Method | Description | | ---------------------------- | -------------------------------------------------------------------- | | `ctx.loadFixtures(dir?)` | Load YAML fixture files from `test/fixtures/` (or a custom path) | | `ctx.loadFeed(name)` | Load a feed by name. Returns `{ generate(feedContext) }` | | `ctx.loadXrpc(name)` | Load an XRPC handler by name. Returns `{ handler(ctx) }` | | `ctx.feedContext(opts?)` | Create a feed context with `limit`, `cursor`, `viewer`, and `params` | | `ctx.db.query(sql, params?)` | Run a SQL query against the in-memory database | | `ctx.db.run(sql, ...params)` | Execute a SQL statement | | `ctx.close()` | Shut down the database | ### Testing a feed ```typescript test('returns statuses in reverse chronological order', async () => { const feed = ctx.loadFeed('recent') const result = await feed.generate(ctx.feedContext({ limit: 10 })) expect(result.items).toHaveLength(6) }) test('respects limit and cursor', async () => { const feed = ctx.loadFeed('recent') const page1 = await feed.generate(ctx.feedContext({ limit: 3 })) expect(page1.cursor).toBeDefined() const page2 = await feed.generate(ctx.feedContext({ limit: 3, cursor: page1.cursor })) expect(page2.items).toHaveLength(3) }) ``` ### Testing an XRPC handler ```typescript test('returns profile for known user', async () => { const handler = ctx.loadXrpc('xyz.statusphere.getProfile') const result = await handler.handler({ params: { actor: 'did:plc:alice' }, }) expect(result.handle).toBe('alice.test') }) ``` ## Fixtures Fixtures are YAML files in `test/fixtures/` that populate the in-memory database before tests run. Each file is named after the table it populates. ### Account fixtures Create a `_repos.yaml` to register test accounts with handles. This file is loaded first, before any collection fixtures: ```yaml # test/fixtures/_repos.yaml - did: did:plc:alice handle: alice.test - did: did:plc:bob handle: bob.test - did: did:plc:carol handle: carol.test ``` If a DID appears in a collection fixture but not in `_repos.yaml`, it is auto-registered with a default handle (`.test`). ### Collection fixtures A file named after a collection inserts records into that collection. Only `did` is required — `uri` and `cid` are auto-generated if omitted: ```yaml # test/fixtures/xyz.statusphere.status.yaml - did: did:plc:alice status: "\U0001F680" createdAt: $now(-5m) - did: did:plc:bob status: "\U0001F9D1\u200D\U0001F4BB" createdAt: $now(-10m) ``` Use the `rkey` field when a record needs a specific record key (e.g., singleton records like profiles) or when other records reference it by URI: ```yaml # test/fixtures/app.bsky.actor.profile.yaml - did: did:plc:alice rkey: self displayName: Alice # test/fixtures/fm.teal.alpha.feed.play.yaml - did: did:plc:alice rkey: '1' trackName: Blinding Lights ``` Without `rkey`, URIs are generated using the record's index (`at:////0`, `at:////1`, etc.). ### Custom table fixtures A YAML file whose name doesn't match a known collection creates a custom table with VARCHAR columns derived from the first record's keys: ```yaml # test/fixtures/my_lookup_table.yaml - key: foo value: bar - key: baz value: qux ``` ### The `$now` helper Use `$now` in fixture values to generate timestamps relative to the current time: | Expression | Result | | ----------- | ------------------- | | `$now` | Current time | | `$now(-5m)` | 5 minutes ago | | `$now(-2h)` | 2 hours ago | | `$now(-1d)` | 1 day ago | | `$now(30s)` | 30 seconds from now | This keeps fixtures time-relative so tests for "recent" feeds and time-based sorting always work. ## Integration tests Integration tests use `startTestServer()` to boot a full HTTP server on a random port: ```typescript import { describe, test, expect, beforeAll, afterAll } from 'vitest' import { startTestServer } from 'hatk/test' let server: Awaited> beforeAll(async () => { server = await startTestServer() await server.loadFixtures() }) afterAll(async () => server?.close()) test('GET /xrpc/dev.hatk.getFeed returns items', async () => { const res = await server.fetch('/xrpc/dev.hatk.getFeed?feed=recent&limit=5') const data = await res.json() expect(data.items.length).toBeGreaterThan(0) }) ``` ### Test server API The test server extends the test context with: | Method | Description | | ---------------------------------- | ------------------------------------------------------------ | | `server.url` | The base URL (e.g., `http://127.0.0.1:54321`) | | `server.fetch(path, init?)` | Fetch a path on the test server | | `server.fetchAs(did, path, init?)` | Fetch as an authenticated user (sets `x-test-viewer` header) | | `server.seed(opts?)` | Get seed helpers for creating records against a real PDS |