AppView in a box as a Vite plugin thing hatk.dev
at main 196 lines 6.6 kB view raw view rendered
1--- 2title: Testing 3description: Run tests and check code quality. 4--- 5 6## `hatk test` 7 8Run your project's test suite. 9 10```bash 11hatk test # Run all tests 12hatk test --unit # Unit tests only 13hatk test --integration # Integration tests only 14hatk test --browser # Playwright browser tests 15``` 16 17| Flag | Description | 18| --------------- | ------------------------------------------------ | 19| `--unit` | Run unit tests in `test/feeds/` and `test/xrpc/` | 20| `--integration` | Run integration tests in `test/integration/` | 21| `--browser` | Run Playwright browser tests in `test/browser/` | 22 23Without flags, all test types are run. 24 25## Writing unit tests 26 27Unit 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. 28 29```typescript 30import { describe, test, expect, beforeAll, afterAll } from 'vitest' 31import { createTestContext } from 'hatk/test' 32 33let ctx: Awaited<ReturnType<typeof createTestContext>> 34 35beforeAll(async () => { 36 ctx = await createTestContext() 37 await ctx.loadFixtures() 38}) 39 40afterAll(async () => ctx?.close()) 41``` 42 43### Test context API 44 45| Method | Description | 46| ---------------------------- | -------------------------------------------------------------------- | 47| `ctx.loadFixtures(dir?)` | Load YAML fixture files from `test/fixtures/` (or a custom path) | 48| `ctx.loadFeed(name)` | Load a feed by name. Returns `{ generate(feedContext) }` | 49| `ctx.loadXrpc(name)` | Load an XRPC handler by name. Returns `{ handler(ctx) }` | 50| `ctx.feedContext(opts?)` | Create a feed context with `limit`, `cursor`, `viewer`, and `params` | 51| `ctx.db.query(sql, params?)` | Run a SQL query against the in-memory database | 52| `ctx.db.run(sql, ...params)` | Execute a SQL statement | 53| `ctx.close()` | Shut down the database | 54 55### Testing a feed 56 57```typescript 58test('returns statuses in reverse chronological order', async () => { 59 const feed = ctx.loadFeed('recent') 60 const result = await feed.generate(ctx.feedContext({ limit: 10 })) 61 expect(result.items).toHaveLength(6) 62}) 63 64test('respects limit and cursor', async () => { 65 const feed = ctx.loadFeed('recent') 66 const page1 = await feed.generate(ctx.feedContext({ limit: 3 })) 67 expect(page1.cursor).toBeDefined() 68 69 const page2 = await feed.generate(ctx.feedContext({ limit: 3, cursor: page1.cursor })) 70 expect(page2.items).toHaveLength(3) 71}) 72``` 73 74### Testing an XRPC handler 75 76```typescript 77test('returns profile for known user', async () => { 78 const handler = ctx.loadXrpc('xyz.statusphere.getProfile') 79 const result = await handler.handler({ 80 params: { actor: 'did:plc:alice' }, 81 }) 82 expect(result.handle).toBe('alice.test') 83}) 84``` 85 86## Fixtures 87 88Fixtures are YAML files in `test/fixtures/` that populate the in-memory database before tests run. Each file is named after the table it populates. 89 90### Account fixtures 91 92Create a `_repos.yaml` to register test accounts with handles. This file is loaded first, before any collection fixtures: 93 94```yaml 95# test/fixtures/_repos.yaml 96- did: did:plc:alice 97 handle: alice.test 98- did: did:plc:bob 99 handle: bob.test 100- did: did:plc:carol 101 handle: carol.test 102``` 103 104If a DID appears in a collection fixture but not in `_repos.yaml`, it is auto-registered with a default handle (`<did-suffix>.test`). 105 106### Collection fixtures 107 108A file named after a collection inserts records into that collection. Only `did` is required — `uri` and `cid` are auto-generated if omitted: 109 110```yaml 111# test/fixtures/xyz.statusphere.status.yaml 112- did: did:plc:alice 113 status: "\U0001F680" 114 createdAt: $now(-5m) 115 116- did: did:plc:bob 117 status: "\U0001F9D1\u200D\U0001F4BB" 118 createdAt: $now(-10m) 119``` 120 121Use 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: 122 123```yaml 124# test/fixtures/app.bsky.actor.profile.yaml 125- did: did:plc:alice 126 rkey: self 127 displayName: Alice 128 129# test/fixtures/fm.teal.alpha.feed.play.yaml 130- did: did:plc:alice 131 rkey: '1' 132 trackName: Blinding Lights 133``` 134 135Without `rkey`, URIs are generated using the record's index (`at://<did>/<collection>/0`, `at://<did>/<collection>/1`, etc.). 136 137### Custom table fixtures 138 139A YAML file whose name doesn't match a known collection creates a custom table with VARCHAR columns derived from the first record's keys: 140 141```yaml 142# test/fixtures/my_lookup_table.yaml 143- key: foo 144 value: bar 145- key: baz 146 value: qux 147``` 148 149### The `$now` helper 150 151Use `$now` in fixture values to generate timestamps relative to the current time: 152 153| Expression | Result | 154| ----------- | ------------------- | 155| `$now` | Current time | 156| `$now(-5m)` | 5 minutes ago | 157| `$now(-2h)` | 2 hours ago | 158| `$now(-1d)` | 1 day ago | 159| `$now(30s)` | 30 seconds from now | 160 161This keeps fixtures time-relative so tests for "recent" feeds and time-based sorting always work. 162 163## Integration tests 164 165Integration tests use `startTestServer()` to boot a full HTTP server on a random port: 166 167```typescript 168import { describe, test, expect, beforeAll, afterAll } from 'vitest' 169import { startTestServer } from 'hatk/test' 170 171let server: Awaited<ReturnType<typeof startTestServer>> 172 173beforeAll(async () => { 174 server = await startTestServer() 175 await server.loadFixtures() 176}) 177 178afterAll(async () => server?.close()) 179 180test('GET /xrpc/dev.hatk.getFeed returns items', async () => { 181 const res = await server.fetch('/xrpc/dev.hatk.getFeed?feed=recent&limit=5') 182 const data = await res.json() 183 expect(data.items.length).toBeGreaterThan(0) 184}) 185``` 186 187### Test server API 188 189The test server extends the test context with: 190 191| Method | Description | 192| ---------------------------------- | ------------------------------------------------------------ | 193| `server.url` | The base URL (e.g., `http://127.0.0.1:54321`) | 194| `server.fetch(path, init?)` | Fetch a path on the test server | 195| `server.fetchAs(did, path, init?)` | Fetch as an authenticated user (sets `x-test-viewer` header) | 196| `server.seed(opts?)` | Get seed helpers for creating records against a real PDS |