fastify-testing — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited fastify-testing (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are guiding the user through testing Fastify applications. Follow these instructions precisely when generating test code, reviewing test files, or advising on Fastify test architecture.
NEVER bind Fastify to a real TCP port during unit or integration tests. Use the .inject() method for all HTTP simulation. Fastify ships with light-my-request built in, so .inject() simulates requests entirely in-process with zero network stack overhead. This means dramatically faster execution and no "address in use" port conflicts when running tests in parallel.
When you see test code that calls app.listen() or binds to a port, flag it immediately and refactor to use .inject() instead.
ALWAYS separate plugin and route registration from the network listener. Structure every Fastify project with this separation:
app.js (or app.ts) that exports a build() or createApp() function. This function constructs the Fastify instance, registers all plugins and routes, then returns the instance without listening.server.js (or server.ts) that imports build(), calls it, and then calls app.listen(). This file contains minimal code and is not tested directly.build().When scaffolding a new Fastify project, always apply this pattern from the start. When reviewing existing code that does not follow this pattern, recommend the refactor as a prerequisite to testability.
// app.js
const Fastify = require('fastify')
async function build(opts = {}) {
const app = Fastify(opts)
app.register(require('./plugins/db'))
app.register(require('./routes/users'), { prefix: '/api/users' })
return app
}
module.exports = build
// server.js
const build = require('./app')
const start = async () => {
const app = await build({ logger: true })
await app.listen({ port: 3000, host: '0.0.0.0' })
}
start()Write all test assertions against .inject() responses. Here is the canonical pattern:
const build = require('./app')
test('GET /api/users returns 200', async () => {
const app = await build()
const response = await app.inject({
method: 'GET',
url: '/api/users'
})
expect(response.statusCode).toBe(200)
expect(JSON.parse(response.payload)).toHaveProperty('users')
})Use these capabilities when constructing test requests:
method: 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', or 'HEAD'.headers object to set any request headers including Authorization, Content-Type, and custom headers.url string (e.g., /api/users?page=2) or pass a query object.payload object for JSON bodies. Fastify serializes it automatically when Content-Type is application/json.headers object using the Cookie header string.statusCode, headers, payload (raw string), and a json() method for parsed JSON..inject() internally calls ready() before dispatching, guaranteeing all plugins are loaded. You do not need to call await app.ready() before injecting.Create a minimal Fastify instance in your test file and register ONLY the specific plugin under test. Use .inject() to verify hooks, routes, and decorators added by that plugin. Fastify's encapsulation model makes this naturally clean because plugins cannot leak side effects into sibling contexts.
const Fastify = require('fastify')
const myPlugin = require('./plugins/auth')
test('auth plugin decorates request with user', async () => {
const app = Fastify()
app.register(myPlugin)
app.get('/test', async (request) => {
return { hasUser: !!request.user }
})
const response = await app.inject({
method: 'GET',
url: '/test',
headers: { authorization: 'Bearer valid-token' }
})
expect(response.json().hasUser).toBe(true)
})Do not register unrelated plugins in plugin isolation tests. If a plugin depends on another plugin, register only the direct dependency, not the entire application plugin tree.
Register the hook inside a test-scoped plugin or directly on the test instance. Use .inject() to trigger the request lifecycle and assert on the response status, headers, or decorated properties.
For onClose hooks specifically, call await app.close() and verify the cleanup side effect (e.g., database connection closed, file handle released).
test('onRequest hook rejects missing API key', async () => {
const app = Fastify()
app.addHook('onRequest', async (request, reply) => {
if (!request.headers['x-api-key']) {
reply.code(401).send({ error: 'Missing API key' })
}
})
app.get('/secure', async () => ({ ok: true }))
const response = await app.inject({ method: 'GET', url: '/secure' })
expect(response.statusCode).toBe(401)
})Follow these rules for every test file:
describe block if tests within that block are truly independent and share no mutable state). Prefer per-test instances to eliminate any risk of state leakage.let app
beforeEach(async () => {
app = await build()
})
afterEach(async () => {
await app.close()
})Because .inject() never binds to a port, all tests can run in parallel without conflict. When configuring your test runner, enable parallel execution for maximum speed. Each test file creates its own isolated Fastify instance, so there is no shared state across files.
If using vitest, set test.pool to 'forks' or 'threads'. If using jest, the default worker-based parallelism works out of the box. If using tap, use --jobs to control concurrency.
Watch for and correct these anti-patterns:
.inject()..inject() to exercise the real code paths. Reserve mocking for external dependencies like databases and third-party APIs.build() and .inject() return promises. Missing await causes tests to pass vacuously.For tests that exercise the full stack including database access:
@testcontainers/postgresql) or in-memory databases (e.g., SQLite with :memory:) to avoid depending on external infrastructure..inject() even for integration tests. There is no reason to bind to a port.beforeEach(async () => {
app = await build({ logger: false })
await seedTestData(app)
})
afterEach(async () => {
await cleanTestData(app)
await app.close()
})
test('POST /api/users creates a user in the database', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/users',
payload: { name: 'Alice', email: '[email protected]' }
})
expect(response.statusCode).toBe(201)
expect(response.json()).toHaveProperty('id')
})~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.