Skip to content

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:

typescript
// 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.ts

Tests mirror source structure. Each source file has a corresponding test file.

Test Patterns

typescript
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

CategoryExamples
Happy pathBasic usage, common patterns
Edge casesEmpty inputs, boundary values, maximum sizes
Error conditionsInvalid arguments, missing dependencies, timeout
ReactivitySubscription notification, batching, diamond dependencies
MemoryCleanup after unsubscribe, GC of unused atoms
TypeScriptType inference (use expectTypeOf from vitest)
SSRScope isolation, serialization, hydration
ConcurrencyBatched updates, async effects, race conditions

Type Testing

typescript
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

  1. Before writing code: Understand the existing tests
  2. While writing code: Add tests for new behavior
  3. After writing code: Run the full test suite, fix failures
  4. For bug fixes: Add a regression test that fails without the fix

Running Tests

Single Package

bash
# Run tests for a specific package
pnpm turbo run test --filter=@stateloom/core

# Run tests with coverage
pnpm turbo run test --filter=@stateloom/core -- --coverage

All Packages

bash
# Run all tests across the monorepo
pnpm test

# Run all tests with coverage
pnpm test -- --coverage

Watch Mode

bash
# Watch mode for a specific package (re-runs on file change)
pnpm turbo run test:watch --filter=@stateloom/core

Running a Single Test File

bash
# Run a specific test file directly with Vitest
pnpm vitest run packages/core/__tests__/signal.test.ts

Coverage 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:

MetricMeaningThreshold
StatementsPercentage of executable statements executed95%
BranchesPercentage of if/else/??/?. branches taken95%
FunctionsPercentage of declared functions called95%
LinesPercentage of source lines executed95%

Checking Coverage Locally

Always check coverage before pushing to CI:

bash
pnpm turbo run test --filter=@stateloom/<package> -- --coverage

If any threshold is below 95%, the test run fails. Fix coverage gaps before committing.

HTML Coverage Report

For a detailed visual report:

bash
pnpm vitest run --coverage --coverage.reporter=html

Open 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:

typescript
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:

bash
pnpm vitest run packages/core/__tests__/signal.test.ts --reporter=verbose

Common Failure Patterns

SymptomLikely CauseFix
Test passes alone, fails in suiteShared mutable state between testsIsolate state in beforeEach or use fresh instances
Timeout in async testMissing await or unresolved promiseCheck all async paths return/resolve
Subscription not calledEquality check preventing notificationVerify Object.is semantics, use new object references
Type test failsIncorrect type inferenceCheck generic constraints, use expectTypeOf diagnostics
Coverage below thresholdUntested branchesRun HTML coverage report, find uncovered lines

Debug with Console Output

typescript
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.