Testing
Assert on templates and properties with the memory sink.
Because the event is data, a test asserts on data — never on a rendered string.
A logger for tests#
import { createLogger, memorySink, LogContext } from '@unhingged/logit';
import { describe, expect, test } from 'bun:test'; // or vitest, jest
test('charging logs the outcome', async () => {
const mem = memorySink();
const log = createLogger({ minimumLevel: 'verbose', sinks: [mem] });
const service = new PaymentService(log);
await service.charge(order);
const event = mem.events.find((e) => e.messageTemplate === 'Charged {OrderId} for {Amount:0.00}');
expect(event).toBeDefined();
expect(event!.level).toBe('info');
expect(event!.properties).toMatchObject({ OrderId: 1042, Amount: 19.5 });
});memorySink() keeps every event (capacity to bound it); mem.messages() gives the rendered messages for a quick check; mem.clear() resets between tests.
Injecting the logger#
Pass a logger in; the default should be the global log so production needs no wiring:
class PaymentService {
constructor(private log: Logger = globalLog.forSource('payments')) {}
}Or reconfigure the global for the whole test file — configure({ sinks: [mem] }) swaps the pipeline under every logger derived from log, including those modules captured at import time.
Asserting on errors#
const failure = mem.events.find((e) => e.level === 'error');
expect(failure!.error).toBeInstanceOf(PaymentError);
expect(failure!.errorProperties).toMatchObject({ name: 'PaymentError', code: 'card_declined' });Context#
LogContext works in tests as in production. To assert a request-scoped property reached an event:
await LogContext.run({ RequestId: 'test-1' }, () => service.handle());
expect(mem.events.every((e) => e.properties.RequestId === 'test-1')).toBe(true);Each test file runs in its own module scope; the AsyncLocalStorage behind LogContext is per process and needs no reset.
Callback sink#
For a spy:
const seen: string[] = [];
const log = createLogger({ sinks: [callbackSink((e) => seen.push(e.messageTemplate))] });Silencing#
createLogger({ sinks: [] }) is a logger that drops everything after capture; createLogger({ minimumLevel: 'fatal', sinks: [] }) drops before it. For the global: configure({ sinks: [] }).
Deterministic output#
Formatter tests fix the timestamp by constructing the event:
import { LogEvent, DEFAULT_DESTRUCTURING, parseTemplate, jsonFormatter } from '@unhingged/logit';
const event = new LogEvent({ timestamp: new Date('2026-10-08T08:12:03.123Z'), level: 'info', template: parseTemplate('Hi {Name}'), properties: { Name: 'Ada' } }, DEFAULT_DESTRUCTURING);
expect(jsonFormatter()(event)).toBe('{"timestamp":"2026-10-08T08:12:03.123Z","level":"info","message":"Hi Ada","template":"Hi {Name}","Name":"Ada"}');HTTP sinks take fetch as an option, so a test hands them a stub and inspects the body.