.agents/skills/apm-integrations/references/testing.md
Create packages/datadog-plugin-<name>/test/index.spec.js:
'use strict'
const assert = require('node:assert/strict')
const agent = require('../../dd-trace/test/plugins/agent')
const { withVersions } = require('../../dd-trace/test/setup/mocha')
const { ANY_STRING } = require('../../../integration-tests/helpers')
describe('Plugin', () => {
describe('<name>', () => {
withVersions('<name>', '<module-name>', (version) => {
let myLib
beforeEach(() => {
return agent.load('<name>')
})
beforeEach(() => {
myLib = require(`../../../versions/<module-name>@${version}`)
})
afterEach(() => {
return agent.close({ ritmReset: false })
})
it('should create a span', async () => {
// Object-based assertion (preferred) — uses assertObjectContains internally
const expectedSpanPromise = agent.assertFirstTraceSpan({
name: '<name>.<operation>',
service: 'test',
type: 'sql',
resource: 'SELECT 1',
meta: {
component: '<name>',
'db.type': '<name>',
},
})
// trigger the instrumented operation
myLib.someOperation()
await expectedSpanPromise
})
it('should create spans with callback assertion', async () => {
// Callback-based assertion — for complex multi-span assertions
const expectedSpanPromise = agent.assertSomeTraces(traces => {
const span = traces[0][0]
assert.strictEqual(span.name, '<name>.<operation>')
assert.strictEqual(span.meta.component, '<name>')
})
// trigger the instrumented operation
myLib.someOperation()
await expectedSpanPromise
})
it('should create spans if an error occurs', async () => {
// Callback-based assertion — for complex multi-span assertions
const expectedSpanPromise = agent.assertFirstTraceSpan({
name: '<name>.<operation>',
service: 'test',
type: 'sql',
resource: 'SELECT 1',
meta: {
component: '<name>',
'db.type': '<name>',
'error.message': '<some error message>',
'error.type': 'TypeError',
'error.stack': ANY_STRING
},
})
// trigger the instrumented operation with an error
myLib.someOperationError()
await expectedSpanPromise
})
})
})
})
agent.load(pluginNames, config, tracerConfig) — starts test agent and loads plugin(s)agent.close({ ritmReset }) — tears down agent (use ritmReset: false to preserve require cache)agent.assertFirstTraceSpan(expectedObject) — asserts traces[0][0] contains the expected properties via assertObjectContains. Preferred for simple single-span assertions.agent.assertFirstTraceSpan(callback) — runs callback with traces[0][0] for custom assertionsagent.assertSomeTraces(callback) — runs callback with full traces array (array of traces, each an array of spans). Use for multi-span or multi-trace assertions.agent.subscribe(handler) — register handler called on every trace payloadagent.unsubscribe(handler) — remove a subscribed handleragent.reload(pluginName, config) — reload a plugin with new configagent.reset() — clear all handlerswithVersions(pluginName, moduleName, cb) — runs tests across installed versionswithNamingSchema(agent, ...) — tests naming schema conventionswithPeerService(agent, ...) — tests peer service tagESM tests verify the plugin works with native ES module imports. They live in packages/datadog-plugin-<name>/test/integration-test/.
Minimal ESM script that initializes the tracer and triggers the instrumented operation:
import 'dd-trace/init.js'
import myLib from '<module-name>'
await myLib.someOperation()
Test that spawns the ESM server and asserts spans arrive:
'use strict'
const assert = require('node:assert/strict')
const {
FakeAgent,
sandboxCwd,
useSandbox,
checkSpansForServiceName,
spawnPluginIntegrationTestProcAndExpectExit,
varySandbox,
} = require('../../../../integration-tests/helpers')
const { withVersions } = require('../../../dd-trace/test/setup/mocha')
describe('esm', () => {
let agent
let proc
let variants
withVersions('<name>', '<module-name>', version => {
useSandbox([`'<module-name>@${version}'`], false, [
'./packages/datadog-plugin-<name>/test/integration-test/*'])
beforeEach(async () => {
agent = await new FakeAgent().start()
})
before(async function () {
variants = varySandbox('server.mjs', '<module-name>', '<namedExport>')
})
afterEach(async () => {
proc && proc.kill()
await agent.stop()
})
for (const variant of varySandbox.VARIANTS) {
it(`is instrumented ${variant}`, async () => {
const res = agent.assertMessageReceived(({ headers, payload }) => {
assert.strictEqual(headers.host, `127.0.0.1:${agent.port}`)
assert.ok(Array.isArray(payload))
assert.strictEqual(checkSpansForServiceName(payload, '<name>.<operation>'), true)
})
proc = await spawnPluginIntegrationTestProcAndExpectExit(sandboxCwd(), variants[variant], agent.port)
await res
}).timeout(20000)
}
})
})
varySandbox(filename, bindingName, namedExport, packageName, byPassDefault) generates three import-style variants (default, star, destructure) to verify all ESM import patternsvarySandbox.VARIANTS is ['default', 'star', 'destructure']byPassDefault: true as fifth argument when the module has no default exportuseSandbox installs package versions into a temp sandbox directoryspawnPluginIntegrationTestProcAndExpectExit spawns node <script> with DD_TRACE_AGENT_PORT set to FakeAgent portit needs generous timeout (e.g., 20000) for sandbox setup and process spawningdd-trace uses a non-standard dependency installation for plugin tests. Libraries under test are installed per-version via yarn services, not through the normal node_modules. The :ci script handles this automatically.
# CI command (preferred) — runs yarn services for dependency installation, then tests
PLUGINS="<name>" npm run test:plugins:ci
# Unit tests only (assumes yarn services already ran)
PLUGINS="<name>" npm run test:plugins
# With external services (e.g., databases, message brokers)
SERVICES="rabbitmq" PLUGINS="amqplib" docker compose up -d $SERVICES
PLUGINS="amqplib" npm run test:plugins:ci
# Filter within plugin tests
PLUGINS="<name>" SPEC="specific.spec.js" npm run test:plugins:ci