Testing
The events your package emits are public behavior: a host filtering on your source field, an agent matching on your error codes, or a support macro keyed on a fix string, all break 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, how to lock error codes down, and the one assertion that keeps the generated error reference current.
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 or an agent's prompt:
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.
Keep the generated reference in sync
If you generate an error reference from the catalog, the committed file drifts the first time someone edits a fix and forgets to rebuild. One test closes that gap: regenerate in memory and compare to what is on disk. Exporting the Catalog has the script and the test; the assertion is expect(committed).toBe(toMarkdown(readCatalog())), and it fails in the pull request that changed the sentence instead of in a consumer's session a week later.
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.