Testing
The events your package emits are public behavior: a host filtering on your source field, or indexing your error codes, breaks when you rename them as surely as when you rename a function. Tests make that visible before the host does. This page covers the two ways to observe events in a test process and how to lock error codes down.
The test process is the host. Nothing about being a library changes the setup: you configure evlog the way an application would, because inside a test that is exactly what your code is.
Collect events with a drain function
A drain function runs synchronously when an event is emitted, so a plain array is enough:
import { initLogger } from 'evlog'
import type { DrainContext, WideEvent } from 'evlog'
const events: WideEvent[] = []
beforeAll(() => {
initLogger({ drain: (ctx: DrainContext) => { events.push(ctx.event) } })
})
// run the operation under test, then assert on the collected events
expect(events[0].source).toBe('mylib')
Assert on the fields you promised the host: the source, the operation, the domain fields you set. Do not assert on console output; pretty printing is presentation, and the drained event is the contract.
Inspect events with the memory drain
The memory drain keeps events in a ring buffer you can read and clear, useful when an operation emits several events and you want them as a list:
import { createMemoryDrain, readMemoryLogs } from 'evlog/memory'
import { initLogger } from 'evlog'
beforeAll(() => {
initLogger({ drain: createMemoryDrain({ store: 'test' }) })
})
// run the operation under test, then give the drain a tick:
// emit() hands events to the drain without awaiting it
await new Promise(resolve => setTimeout(resolve, 0))
const events = readMemoryLogs({ store: 'test', filter: e => e.source === 'mylib' })
Lock error codes down
Catalog errors are wire format (see Structured Errors), so assert through the factory rather than a string literal. A rename then fails in your test suite instead of in a host's alert:
import { errors } from '../src/errors'
// in the test asserting throw behavior:
expect(() => charge(-5)).toThrow()
expect(errors.INSUFFICIENT_FUNDS.code).toBe('mylib.INSUFFICIENT_FUNDS')
The string literal lives in one line, and it states the wire format your package promised. If you delete or rename the catalog entry, the test stops compiling at errors.INSUFFICIENT_FUNDS, which is the point.
For emitted events carrying an error, the code lands in the event the same way, so the same assertion works against a collected event's error tree.
What sampling does to your tests
The test host configures sampling like any host. Defaults keep everything (minLevel: 'debug', no rates), so events arrive. If a test seems to lose events, check whether an earlier test in the same process called initLogger() with rates or enabled: false: configuration is process-wide and the last call wins, which is the same mechanism Emitting Events warns library code about. Sampling documents what a real host can do to your events, including dropping them entirely.