← All reusables

test

utility

Installation

npx jsrepo add github/reatom/reusables test

Dependencies

  • vitest ^4.0.18

Documentation

Test Harness

A reusable testing utilities on top of Vitest API for Reatom applications that provides enhanced testing capabilities with automatic context management and subscription mocking.

Note: This reusable is built on top of Vitest, but you may choose to rewrite it to work with other test runners.

Usage

After installation, import the test utilities:

import { test, expect, subscribe } from './src/reatom/test'

Or use an alias (see Configuration section):

import { test, expect, subscribe } from 'test'

Features

  • Automatic Context Management: Test functions are automatically wrapped in context.start(), eliminating "missed context" errors
  • Context Isolation: Each test runs in a fresh context, preventing state leakage between tests
  • Mock Subscriptions: Track atom updates and action calls using Vitest's mock functionality
  • Full Vitest Compatibility: All standard Vitest utilities are re-exported for convenience

API Reference

test()

Enhanced version of Vitest's test function that automatically wraps test callbacks in Reatom's context.

NOTE: the test methods (test.skip, test.each and so on) are not supported, use viTest export for this cases (viTest.skip, viTest.each and so on)

Signature:

test(name: string, fn: () => void | Promise<void>): void

Example:

import { test, expect } from 'test'
import { atom } from '@reatom/core'

test('atom updates correctly', () => {
  const counter = atom(0, 'counter')
  counter.set(5)
  expect(counter()).toBe(5)
})

Key Benefits:

  • No need to manually call context.start()
  • Automatic context isolation between tests
  • Works with both sync and async tests
  • Preserves all Vitest functionality

subscribe()

Creates a mock subscriber for an atom or action that tracks all updates using Vitest's mock functionality.

For atoms, the mock is called with each new state value. For actions, the mock is called with the action's params (not the ActionState array) and returns the payload, making it easy to test action results.

Signatures:

// For actions — mock receives params, returns payload
subscribe<Params extends any[], Payload>(
  target: Action<Params, Payload>,
  cb?: (...params: Params) => Payload
): Mock<(...params: Params) => Payload> & { unsubscribe: Unsubscribe }

// For atoms — mock receives state
subscribe<State, T extends (state: State) => any>(
  target: AtomLike<State>,
  cb?: T
): Mock<T> & { unsubscribe: Unsubscribe }

Parameters:

  • target: A Reatom atom, computed value, or action to subscribe to
  • cb: Optional callback function to execute on each update (defaults to noop)

Returns: A Vitest mock function with an attached unsubscribe method

Atom example

import { test, expect, subscribe } from 'test'
import { atom, sleep, wrap } from '@reatom/core'

test('subscribe captures batched updates', async () => {
  const counter = atom(0, 'counter')
  const sub = subscribe(counter)

  counter.set(1)
  await wrap(sleep())
  counter.set(2)
  await wrap(sleep())
  counter.set(3)
  counter.set(4)
  await wrap(sleep())

  expect(sub).toHaveBeenCalledTimes(4)
  expect(sub).toHaveBeenLastCalledWith(4)
  expect(sub.mock.calls).toEqual([[0], [1], [2], [4]])

  sub.unsubscribe()
})

Atom with custom callback

test('subscribe with custom callback', () => {
  const counter = atom(0, 'counter')
  const results: number[] = []

  const sub = subscribe(counter, (val) => {
    results.push(val * 2)
  })

  counter.set(5)
  counter.set(10)

  expect(results).toEqual([0, 10, 20])
  sub.unsubscribe()
})

Action example

import { test, expect, subscribe } from 'test'
import { action } from '@reatom/core'

test('subscribe tracks action calls with params and payload', () => {
  const double = action((x: number) => x * 2, 'double')
  const sub = subscribe(double)

  double(5)
  double(10)

  expect(sub).toHaveBeenCalledTimes(2)
  expect(sub).toHaveBeenCalledWith(5)
  expect(sub).toHaveBeenLastCalledWith(10)
  expect(sub).toHaveLastReturnedWith(20)
  expect(sub.mock.results).toEqual([
    { type: 'return', value: 10 },
    { type: 'return', value: 20 },
  ])

  sub.unsubscribe()
})

Action with multiple params

test('subscribe tracks multi-param actions', () => {
  const add = action((a: number, b: number) => a + b, 'add')
  const sub = subscribe(add)

  add(2, 3)

  expect(sub).toHaveBeenCalledWith(2, 3)
  expect(sub).toHaveLastReturnedWith(5)

  sub.unsubscribe()
})

silentQueuesErrors()

Silences unhandled errors in Reatom's queues to prevent console noise during tests that intentionally check for errors. This function overrides the default queue push behavior to catch and ignore any errors thrown within queue callbacks.

Signature:

silentQueuesErrors(): void

Example:

import { test, silentQueuesErrors } from 'test'

test('handles errors silently', () => {
  silentQueuesErrors()
  // Now errors in Reatom queues won't log to console
})

Re-exported Utilities

The following Vitest utilities are re-exported for convenience:

  • expect
  • describe
  • beforeEach
  • afterEach
  • beforeAll
  • afterAll
  • vi
  • expectTypeOf
  • original test is aliased as viTest
  • All Vitest types

Usage Patterns

Basic Atom Testing

import { test, expect } from 'test'
import { atom } from '@reatom/core'

test('atom initialization', () => {
  const counter = atom(0, 'counter')
  expect(counter()).toBe(0)
})

test('atom updates', () => {
  const counter = atom(0, 'counter')
  counter.set(5)
  expect(counter()).toBe(5)
})

Computed Atom Testing

import { test, expect, subscribe } from 'test'
import { atom, computed } from '@reatom/core'

test('computed atom updates', () => {
  const counter = atom(0, 'counter')
  const doubled = computed(() => counter() * 2, 'doubled')
  const sub = subscribe(doubled)

  expect(doubled()).toBe(0)

  counter.set(5)
  expect(doubled()).toBe(10)
  expect(sub).toHaveBeenLastCalledWith(10)

  sub.unsubscribe()
})

Testing Update Sequences

import { test, expect, subscribe } from 'test'
import { atom } from '@reatom/core'

test('tracks all updates in order', () => {
  const counter = atom(0, 'counter')
  const sub = subscribe(counter)

  counter.set(1)
  counter.set(2)
  counter.set(3)

  expect(sub.mock.calls).toEqual([[0], [1], [2], [3]])
  sub.unsubscribe()
})

Action Testing

import { test, expect, subscribe } from 'test'
import { action } from '@reatom/core'

test('action payload tracking', () => {
  const fetchUser = action((id: string) => ({ id, name: 'User' }), 'fetchUser')
  const sub = subscribe(fetchUser)

  const result = fetchUser('123')

  // verify the action was called with correct params
  expect(sub).toHaveBeenCalledWith('123')
  // verify the returned payload
  expect(sub).toHaveLastReturnedWith({ id: '123', name: 'User' })

  sub.unsubscribe()
})

Async Testing

import { test, expect } from 'test'
import { atom } from '@reatom/core'

test('handles async operations', async () => {
  const counter = atom(0, 'counter')

  await Promise.resolve()
  counter.set(10)

  expect(counter()).toBe(10)
})

Migration from Standard Vitest

If you're migrating existing tests, the main changes are:

  1. Import test from test instead of vitest
  2. Remove manual context.start() calls and clearContext call (if used)
  3. Remove manual context.reset() from beforeEach hooks (if used)
  4. Use subscribe() helper instead of manual atom subscription + vi.fn()

It your application entrypoint started with reatom routing system, you can run it just with the url atom reading - urlAtom()

Configuration

To set up the test alias in your project (optional but recommended):

vitest.config.ts:

import { defineConfig } from 'vitest/config'

export default defineConfig({
  resolve: {
    alias: {
      test: './src/reatom/test/test.ts',
    },
  },
})

tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "test": ["./src/reatom/test/test.ts"]
    }
  }
}

Note: The exact path may vary depending on your jsrepo configuration. Adjust the paths above to match where jsrepo installed the files in your project.

Examples

The test harness is used throughout the @reatom/reusables project.

Why Use This Test Harness?

  1. Universal implementation: Easy to change Vitest to any other test runner
  2. Reduces Boilerplate: No need to manually manage context in every test
  3. Prevents Common Errors: Eliminates "missed context" errors that are common in Reatom testing
  4. Better Debugging: Mock subscriptions provide clear visibility into atom updates
  5. Isolation Guarantee: Each test runs in a fresh context, preventing flaky tests
  6. Follows Core Patterns: Aligned with testing patterns used in @reatom/core