Testing Guidelines
These standards govern all tests in the @stateloom/* monorepo. Every code change must include tests that meet these requirements.
Coverage Target: 95%+
Every package must maintain near-100% test coverage:
// vitest.config.ts per package
export default defineConfig({
test: {
coverage: {
provider: 'v8',
thresholds: {
statements: 95,
branches: 95,
functions: 95,
lines: 95,
},
},
},
});Test File Structure
packages/core/
├── src/
│ ├── signal.ts
│ └── computed.ts
└── __tests__/
├── signal.test.ts
└── computed.test.tsTests mirror source structure. Each source file has a corresponding test file.
Test Patterns
import { describe, it, expect, vi } from 'vitest';
import { signal, computed, effect } from '../src/index.js';
describe('signal', () => {
it('returns initial value', () => {
const s = signal(42);
expect(s.get()).toBe(42);
});
it('notifies subscribers on set', () => {
const s = signal(0);
const listener = vi.fn();
s.subscribe(listener);
s.set(1);
expect(listener).toHaveBeenCalledWith(1);
});
it('does not notify when value is equal (Object.is)', () => {
const s = signal(0);
const listener = vi.fn();
s.subscribe(listener);
s.set(0);
expect(listener).not.toHaveBeenCalled();
});
});What to Test
| Category | Examples |
|---|---|
| Happy path | Basic usage, common patterns |
| Edge cases | Empty inputs, boundary values, maximum sizes |
| Error conditions | Invalid arguments, missing dependencies, timeout |
| Reactivity | Subscription notification, batching, diamond dependencies |
| Memory | Cleanup after unsubscribe, GC of unused atoms |
| TypeScript | Type inference (use expectTypeOf from vitest) |
| SSR | Scope isolation, serialization, hydration |
| Concurrency | Batched updates, async effects, race conditions |
Type Testing
import { expectTypeOf } from 'vitest';
import { signal, computed } from '../src/index.js';
it('infers signal type from initial value', () => {
const s = signal(42);
expectTypeOf(s.get()).toEqualTypeOf<number>();
});
it('infers computed type from return value', () => {
const s = signal('hello');
const c = computed(() => s.get().length);
expectTypeOf(c.get()).toEqualTypeOf<number>();
});Test Requirements for Every Change
- Before writing code: Understand the existing tests
- While writing code: Add tests for new behavior
- After writing code: Run the full test suite, fix failures
- For bug fixes: Add a regression test that fails without the fix
Running Tests
Single Package
# Run tests for a specific package
pnpm turbo run test --filter=@stateloom/core
# Run tests with coverage
pnpm turbo run test --filter=@stateloom/core -- --coverageAll Packages
# Run all tests across the monorepo
pnpm test
# Run all tests with coverage
pnpm test -- --coverageWatch Mode
# Watch mode for a specific package (re-runs on file change)
pnpm turbo run test:watch --filter=@stateloom/coreRunning a Single Test File
# Run a specific test file directly with Vitest
pnpm vitest run packages/core/__tests__/signal.test.tsCoverage Reports
Reading Coverage Output
After running tests with --coverage, Vitest outputs a summary table:
--------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
--------------------|---------|----------|---------|---------|
All files | 97.14 | 95.83 | 96.00 | 97.14 |
signal.ts | 98.00 | 96.00 | 100.00 | 98.00 |
computed.ts | 96.00 | 95.00 | 92.30 | 96.00 |
--------------------|---------|----------|---------|---------|Each column must meet the 95% threshold:
| Metric | Meaning | Threshold |
|---|---|---|
| Statements | Percentage of executable statements executed | 95% |
| Branches | Percentage of if/else/??/?. branches taken | 95% |
| Functions | Percentage of declared functions called | 95% |
| Lines | Percentage of source lines executed | 95% |
Checking Coverage Locally
Always check coverage before pushing to CI:
pnpm turbo run test --filter=@stateloom/<package> -- --coverageIf any threshold is below 95%, the test run fails. Fix coverage gaps before committing.
HTML Coverage Report
For a detailed visual report:
pnpm vitest run --coverage --coverage.reporter=htmlOpen coverage/index.html to see per-file, per-line coverage details. This helps identify exactly which branches or statements are uncovered.
Debugging Failing Tests
Using it.only to Isolate Failures
Focus on a single test by adding .only:
it.only('the failing test', () => {
// Only this test runs in the file
});Remember to remove .only before committing.
Verbose Output
Use the verbose reporter for detailed failure information:
pnpm vitest run packages/core/__tests__/signal.test.ts --reporter=verboseCommon Failure Patterns
| Symptom | Likely Cause | Fix |
|---|---|---|
| Test passes alone, fails in suite | Shared mutable state between tests | Isolate state in beforeEach or use fresh instances |
| Timeout in async test | Missing await or unresolved promise | Check all async paths return/resolve |
| Subscription not called | Equality check preventing notification | Verify Object.is semantics, use new object references |
| Type test fails | Incorrect type inference | Check generic constraints, use expectTypeOf diagnostics |
| Coverage below threshold | Untested branches | Run HTML coverage report, find uncovered lines |
Debug with Console Output
it('debugging example', () => {
const s = signal(0);
s.set(1);
console.log('Signal value:', s.get()); // visible in test output
expect(s.get()).toBe(1);
});Use --reporter=verbose to see console.log output alongside test results.