|
| 1 | +--- |
| 2 | +description: Prefer whole-result-shape assertions with explicit select projections in sql-orm-client tests |
| 3 | +globs: ["packages/3-extensions/sql-orm-client/**/*.test.ts", "test/integration/test/sql-orm-client/**/*.test.ts"] |
| 4 | +alwaysApply: false |
| 5 | +--- |
| 6 | + |
| 7 | +# Assert the whole result shape, with explicit projections |
| 8 | + |
| 9 | +In `sql-orm-client` tests (unit and integration), assert the **entire** result with `toEqual` |
| 10 | +(or an inline/file snapshot), and pin the projected columns with explicit `.select(...)` (varargs: |
| 11 | +`.select('id', 'name')`) on the root collection **and** on every included relation. Order |
| 12 | +deterministically with |
| 13 | +`.orderBy((c) => c.<baseColumn>.asc())` so `toEqual` on arrays is reliable. |
| 14 | + |
| 15 | +Avoid partial matchers (`toMatchObject`, `toHaveProperty` / `not.toHaveProperty`, lone |
| 16 | +single-field `toBe`/`toEqual`) as the primary assertion for a query result, and avoid `toEqual` |
| 17 | +on a full model row with no `select`. |
| 18 | + |
| 19 | +## Why |
| 20 | + |
| 21 | +Two failure modes this prevents: |
| 22 | + |
| 23 | +1. **Partial matchers pass silently on wrong shapes.** `toMatchObject({ id: 1, role: 'admin' })` |
| 24 | + succeeds even if the row carries an extra field it shouldn't, a sibling-variant field that |
| 25 | + should have been dropped, or a misspelled key elsewhere. The result's *shape* is the contract; |
| 26 | + assert all of it. |
| 27 | +2. **`toEqual` without `select` couples every test to the full model field set.** Adding one field |
| 28 | + to a model then breaks every test that asserted a full row of it — tests far from the change. |
| 29 | + An explicit `.select(...)` makes the asserted columns intentional, so unrelated field |
| 30 | + additions don't ripple into unrelated tests, while `toEqual` still catches any wrong/missing |
| 31 | + value *within* the selected shape. |
| 32 | + |
| 33 | +Together: `select` + `toEqual` is both **complete** (catches extra/missing/wrong fields in the |
| 34 | +projected shape) and **stable** (immune to unrelated model growth). |
| 35 | + |
| 36 | +## Good |
| 37 | + |
| 38 | +```ts |
| 39 | +const rows = await db.orm.Account |
| 40 | + .select('id', 'name') |
| 41 | + .orderBy((a) => a.id.asc()) |
| 42 | + .include('members', (m) => m.select('id', 'kind', 'role', 'plan').orderBy((u) => u.id.asc())) |
| 43 | + .all(); |
| 44 | + |
| 45 | +expect(rows).toEqual([ |
| 46 | + { id: 1, name: 'Acme', members: [ |
| 47 | + { id: 1, kind: 'admin', role: 'superadmin' }, // variant fields surface per the row's variant; |
| 48 | + { id: 2, kind: 'regular', plan: 'free' }, // pinning them locks the variant shape |
| 49 | + ] }, |
| 50 | + { id: 2, name: 'Empty', members: [] }, |
| 51 | +]); |
| 52 | +``` |
| 53 | + |
| 54 | +## Avoid |
| 55 | + |
| 56 | +```ts |
| 57 | +const member = members.find((m) => m.id === 1)!; |
| 58 | +expect(member).toMatchObject({ id: 1, role: 'admin' }); // passes even if `member` has stray fields |
| 59 | +expect(member).not.toHaveProperty('plan'); // enumerating absences ≠ asserting the shape |
| 60 | +``` |
| 61 | + |
| 62 | +## Notes |
| 63 | + |
| 64 | +- **Polymorphic includes:** variant-specific fields surface according to each row's variant. Pin |
| 65 | + them with `select` + `toEqual` so the per-variant shape (e.g. admin rows carry `role`, regular |
| 66 | + rows carry `plan`) is asserted, not assumed. |
| 67 | +- **Determinism:** order by a **base-table** column (typically `id`). Don't order by a variant |
| 68 | + table's column on a variant-narrowed collection unless that path is the thing under test. |
| 69 | +- **Snapshots** (`toMatchInlineSnapshot`) are an acceptable alternative to a hand-written `toEqual` |
| 70 | + for large shapes, but still pair them with explicit `select` so the snapshot is stable. |
| 71 | +- This is about *result-shape* assertions. Asserting a single scalar (a count, a thrown error |
| 72 | + code, a boolean) with `toBe`/`expect().rejects` is fine and expected. |
| 73 | +- **Relationship to `prefer-object-matcher.mdc`:** that rule consolidates scattered |
| 74 | + `expect().toBe()` calls into one matcher repo-wide. This rule is the stricter, sql-orm-client |
| 75 | + specialization for *query results*: the result row is the contract, so assert it **completely** |
| 76 | + with `toEqual` + `select` rather than partially with `toMatchObject`. `toMatchObject` is still |
| 77 | + fine for the non-result, constructed-object cases `prefer-object-matcher` targets. |
0 commit comments