Test file conventions: setup functions, factories, Result assertion helpers, organization, type testing, naming, and pruning low-value tests. Use when: "write tests", "add a test", "fix this test", "delete tests", "prune tests", "audit tests", or modifying *.test.ts files.
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Load these on demand based on what you're working on:
@ts-expect-error, bun:test type strategy, no as any), read references/type-testing.mdsetup() patterns, composable setup, beforeEach avoidance, shared schemas), read references/setup-pattern.mddescribe() boundaries, helper-over-nesting), read references/test-structure.mdExternal reading:
expectTypeOf for type testingshoehorn : partial mocks for test ergonomicsRelated Skills: See
services-layerfor the service patterns being tested. Seetypescriptfor type testing conventions.
When a test asserts a wellcrafted Result, use expectOk and expectErr
from wellcrafted/testing instead of hand-rolled error checks.
import { expectErr, expectOk } from 'wellcrafted/testing';
const data = expectOk(await service.doThing());
expect(data.id).toBe('1');
const error = expectErr(await service.doThing({ invalid: true }));
expect(error.name).toBe('InvalidInput');Avoid local helper clones, expect(error).toBeNull() success checks, and
if (error) throw ... unwrapping when the value is a wellcrafted Result.
This rule does not apply to plain response bodies, UI snapshots, or other
objects that merely have an error property.
Two distinct file extensions, two distinct purposes:
*.test.ts : asserts behavior with expect(). Runs under bun test
(repo default, CI). A test file without at least one expect() call does
not belong under this extension.*.bench.ts : measures and reports. Prints tables, timings, or
storage sizes. Runs under bun bench only. No assertions required
(perf thresholds on shared hardware flake; prefer visual trends).A single file is one or the other, never both. Benchmarks live under
src/__benchmarks__/ within a package; tests are colocated with the module
they cover. The bun test default-discovery glob picks up only *.test.ts
and friends, so renaming a report from .test.ts → .bench.ts is what
excludes it from CI.
Every .test.ts file MUST start with a JSDoc block explaining what is being tested and the key behaviors verified. This serves as documentation for the module's contract.
/**
* [Module Name] Tests
*
* [1-3 sentences explaining what this file tests and why these tests matter.]
*
* Key behaviors:
* - [Behavior 1]
* - [Behavior 2]
* - [Behavior 3]
*
* See also:
* - `related-file.test.ts` for [related aspect]
*//**
* Cell-Level LWW CRDT Sync Tests
*
* Verifies cell-level LWW conflict resolution where each field
* has its own timestamp. Unlike row-level LWW, concurrent edits to
* DIFFERENT fields merge independently.
*
* Key behaviors:
* - Concurrent edits to SAME field: latest timestamp wins
* - Concurrent edits to DIFFERENT fields: BOTH preserved (merge)
* - Delete removes all cells for a row
*/// Tests for create-tablesFor long test files (100+ lines), use comment headers to separate logical sections:
// ============================================================================
// MESSAGE_SYNC Tests
// ============================================================================When a module has distinct behavioral aspects, split into focused test files rather than one monolithic file:
| Pattern | Use Case |
|---|---|
{module}.test.ts | Core CRUD behavior, happy paths, edge cases |
{module}.types.test.ts | Type inference verification, negative type tests |
{module}.{scenario}.test.ts | Specific scenarios (CRDT sync, offline, integration) |
Test descriptions MUST be behavior assertions, not vague descriptions. The name should tell you what broke when the test fails.
test('upsert stores row and get retrieves it', () => { ... });
test('filter returns only published posts', () => { ... });
test('concurrent edits to different fields: both preserved', () => { ... });
test('delete vs update race: update wins (rightmost entry)', () => { ... });
test('observer fires once per transaction, not per operation', () => { ... });
test('get() throws for undefined tables with helpful message', () => { ... });test('should work correctly', () => { ... }); // What works? What's correct?
test('should handle batch operations', () => { ... }); // Handle how?
test('basic test', () => { ... }); // Says nothing
test('should create and retrieve rows correctly', () => { ... }); // Vague "correctly"{action} {outcome} [condition]"upsert stores row and get retrieves it"
^^^^^^ ^^^^^^^^^^ ^^^ ^^^^^^^^^^^^^
action outcome action outcome
"observer fires once per transaction, not per operation"
^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
subject outcome condition
"get() returns not_found for non-existent rows"
^^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
action outcome conditioncb12bcc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.