Skip to main content

Testing

Sawdust's service-locator design makes tests deterministic: swap in a mock, assert the calls, reset. This works in Jest and Vitest alike — the API is identical (jest.fnvi.fn).

Reset between tests

Both the logger and RUM locators expose reset helpers. Call them in a beforeEach / afterEach pair so state never leaks across suites.

import { setLogger, resetLoggerLocator, noopLogger } from '@cues/sawdust'
import { setRumClient, resetRumClientLocator, createRumClient } from '@cues/sawdust-datadog/rum'

beforeEach(() => {
resetLoggerLocator()
resetRumClientLocator()
setLogger(noopLogger)
setRumClient(createRumClient())
})

afterEach(() => {
resetLoggerLocator()
resetRumClientLocator()
})

This guarantees the logger façade always delegates to a noop instance unless a test explicitly installs another one.

Mock the logger

Stub the minimal BaseLogger contract or the full LoggerImplementation, depending on what your code calls. For utility libraries the base contract is usually enough.

function createMockLogger() {
const calls: Array<{ level: string; args: unknown[] }> = []
const make = (level: string) => (...args: unknown[]) => calls.push({ level, args })

const logger = {
trace: make('trace'),
debug: make('debug'),
info: make('info'),
warn: make('warn'),
error: make('error'),
fatal: make('fatal'),
child: () => logger,
}
return { calls, logger }
}

test('logs during work', () => {
const mock = createMockLogger()
setLogger(mock.logger)

doWork()

expect(mock.calls.some((c) => c.args[0] === 'work done')).toBe(true)
})

Need the full fluent API? Start from noopLogger and spy on it, or inject a lightweight implementation that satisfies LoggerImplementation.

Mock RUM

The RUM locator behaves the same way. Inject a noop client or stub the methods you expect to fire (see the full stub in the RUM guide).

import { setRumClient, resetRumClientLocator } from '@cues/sawdust-datadog/rum'

beforeEach(() => {
resetRumClientLocator()
setRumClient(createMockRum())
})

Test utilities

  • Spying on calls — because the façade always points at the current singleton, spying on getLogger() / getRumClient() gives you the live instance.
  • Temporary overrides — call setLogger(other) / setRumClient(other) mid-test; always reset… afterward to avoid leaks.
  • Async context — when testing request-scoped logging, use withRequestContext with a mocked logger and assert the child calls (see Request Scope).

Keep resets symmetrical and the service locator stays predictable across suites.