Testing
Testing is one of the most important parts of developing an application. Fastify is very flexible when it comes to testing and is compatible with most testing frameworks (such as Node Test Runner, which is used in the examples below).
Application
Let's cd
into a fresh directory called 'testing-example' and type npm init
-y
in our terminal.
Run npm i fastify && npm i pino-pretty -D
Separating concerns makes testing easy
First, we are going to separate our application code from our server code:
app.js:
'use strict'
const fastify = require('fastify')
function build(opts={}) {
const app = fastify(opts)
app.get('/', async function (request, reply) {
return { hello: 'world' }
})
return app
}
module.exports = build
server.js:
'use strict'
const server = require('./app')({
logger: {
level: 'info',
transport: {
target: 'pino-pretty'
}
}
})
server.listen({ port: 3000 }, (err, address) => {
if (err) {
server.log.error(err)
process.exit(1)
}
})
Benefits of using fastify.inject()
Fastify comes with built-in support for fake HTTP injection thanks to
light-my-request
.
Before introducing any tests, we will use the .inject
method to make a fake
request to our route:
app.test.js:
'use strict'
const build = require('./app')
const test = async () => {
const app = build()
const response = await app.inject({
method: 'GET',
url: '/'
})
console.log('status code: ', response.statusCode)
console.log('body: ', response.body)
}
test()
First, our code will run inside an asynchronous function, giving us access to async/await.
.inject
ensures all registered plugins have booted up and our application is
ready to test. Finally, we pass the request method we want to use and a route.
Using await we can store the response without a callback.
Run the test file in your terminal node app.test.js
status code: 200
body: {"hello":"world"}
Testing with HTTP injection
Now we can replace our console.log
calls with actual tests!
In your package.json
change the "test" script to:
"test": "node --test --watch"
app.test.js:
'use strict'
const { test } = require('node:test')
const build = require('./app')
test('requests the "/" route', async t => {
t.plan(1)
const app = build()
const response = await app.inject({
method: 'GET',
url: '/'
})
t.assert.strictEqual(response.statusCode, 200, 'returns a status code of 200')
})
Finally, run npm test
in the terminal and see your test results!
The inject
method can do much more than a simple GET request to a URL:
fastify.inject({
method: String,
url: String,
query: Object,
payload: Object,
headers: Object,
cookies: Object
}, (error, response) => {
// your tests
})
.inject
methods can also be chained by omitting the callback function:
fastify
.inject()
.get('/')
.headers({ foo: 'bar' })
.query({ foo: 'bar' })
.end((err, res) => { // the .end call will trigger the request
console.log(res.payload)
})
or in the promisified version
fastify
.inject({
method: String,
url: String,
query: Object,
payload: Object,
headers: Object,
cookies: Object
})
.then(response => {
// your tests
})
.catch(err => {
// handle error
})
Async await is supported as well!
try {
const res = await fastify.inject({ method: String, url: String, payload: Object, headers: Object })
// your tests
} catch (err) {
// handle error
}
Another Example:
app.js
const Fastify = require('fastify')
function buildFastify () {
const fastify = Fastify()
fastify.get('/', function (request, reply) {
reply.send({ hello: 'world' })
})
return fastify
}
module.exports = buildFastify
test.js
const { test } = require('node:test')
const buildFastify = require('./app')
test('GET `/` route', t => {
t.plan(4)
const fastify = buildFastify()
// At the end of your tests it is highly recommended to call `.close()`
// to ensure that all connections to external services get closed.
t.after(() => fastify.close())
fastify.inject({
method: 'GET',
url: '/'
}, (err, response) => {
t.assert.ifError(err)
t.assert.strictEqual(response.statusCode, 200)
t.assert.strictEqual(response.headers['content-type'], 'application/json; charset=utf-8')
t.assert.deepStrictEqual(response.json(), { hello: 'world' })
})
})
Testing with a running server
Fastify can also be tested after starting the server with fastify.listen()
or
after initializing routes and plugins with fastify.ready()
.
Example:
Uses app.js from the previous example.
test-listen.js (testing with undici
)
const { test } = require('node:test')
const { Client } = require('undici')
const buildFastify = require('./app')
test('should work with undici', async t => {
t.plan(2)
const fastify = buildFastify()
await fastify.listen()
const client = new Client(
'http://localhost:' + fastify.server.address().port, {
keepAliveTimeout: 10,
keepAliveMaxTimeout: 10
}
)
t.after(() => {
fastify.close()
client.close()
})
const response = await client.request({ method: 'GET', path: '/' })
t.assert.strictEqual(await response.body.text(), '{"hello":"world"}')
t.assert.strictEqual(response.statusCode, 200)
})
Alternatively, starting with Node.js 18,
fetch
may be used without requiring any extra dependencies:
test-listen.js
const { test } = require('node:test')
const buildFastify = require('./app')
test('should work with fetch', async t => {
t.plan(3)
const fastify = buildFastify()
t.after(() => fastify.close())
await fastify.listen()
const response = await fetch(
'http://localhost:' + fastify.server.address().port
)
t.assert.strictEqual(response.status, 200)
t.assert.strictEqual(
response.headers.get('content-type'),
'application/json; charset=utf-8'
)
const jsonResult = await response.json()
t.assert.strictEqual(jsonResult.hello, 'world')
})
test-ready.js (testing with
SuperTest
)
const { test } = require('node:test')
const supertest = require('supertest')
const buildFastify = require('./app')
test('GET `/` route', async (t) => {
const fastify = buildFastify()
t.after(() => fastify.close())
await fastify.ready()
const response = await supertest(fastify.server)
.get('/')
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8')
t.assert.deepStrictEqual(response.body, { hello: 'world' })
})
How to inspect node tests
- Isolate your test by passing the
{only: true}
option
test('should ...', {only: true}, t => ...)
- Run
node --test
> node --test --test-only --node-arg=--inspect-brk test/<test-file.test.js>
--test-only
specifies to run tests with theonly
option enabled--node-arg=--inspect-brk
will launch the node debugger
- In VS Code, create and launch a
Node.js: Attach
debug configuration. No modification should be necessary.
Now you should be able to step through your test file (and the rest of
Fastify
) in your code editor.
Plugins
Let's cd
into a fresh directory called 'testing-plugin-example' and type npm init
-y
in our terminal.
Run npm i fastify fastify-plugin
plugin/myFirstPlugin.js:
const fP = require("fastify-plugin")
async function myPlugin(fastify, options) {
fastify.decorateRequest("helloRequest", "Hello World")
fastify.decorate("helloInstance", "Hello Fastify Instance")
}
module.exports = fP(myPlugin)
A basic example of a Plugin. See Plugin Guide
test/myFirstPlugin.test.js:
const Fastify = require("fastify");
const { test } = require("node:test");
const myPlugin = require("../plugin/myFirstPlugin");
test("Test the Plugin Route", async t => {
// Create a mock fastify application to test the plugin
const fastify = Fastify()
fastify.register(myPlugin)
// Add an endpoint of your choice
fastify.get("/", async (request, reply) => {
return ({ message: request.helloRequest })
})
// Use fastify.inject to fake a HTTP Request
const fastifyResponse = await fastify.inject({
method: "GET",
url: "/"
})
console.log('status code: ', fastifyResponse.statusCode)
console.log('body: ', fastifyResponse.body)
})
Learn more about fastify.inject()
.
Run the test file in your terminal node test/myFirstPlugin.test.js
status code: 200
body: {"message":"Hello World"}
Now we can replace our console.log
calls with actual tests!
In your package.json
change the "test" script to:
"test": "node --test --watch"
Create the test for the endpoint.
test/myFirstPlugin.test.js:
const Fastify = require("fastify");
const { test } = require("node:test");
const myPlugin = require("../plugin/myFirstPlugin");
test("Test the Plugin Route", async t => {
// Specifies the number of test
t.plan(2)
const fastify = Fastify()
fastify.register(myPlugin)
fastify.get("/", async (request, reply) => {
return ({ message: request.helloRequest })
})
const fastifyResponse = await fastify.inject({
method: "GET",
url: "/"
})
t.assert.strictEqual(fastifyResponse.statusCode, 200)
t.assert.deepStrictEqual(JSON.parse(fastifyResponse.body), { message: "Hello World" })
})
Finally, run npm test
in the terminal and see your test results!
Test the .decorate()
and .decorateRequest()
.
test/myFirstPlugin.test.js:
const Fastify = require("fastify");
const { test }= require("node:test");
const myPlugin = require("../plugin/myFirstPlugin");
test("Test the Plugin Route", async t => {
t.plan(5)
const fastify = Fastify()
fastify.register(myPlugin)
fastify.get("/", async (request, reply) => {
// Testing the fastify decorators
t.assert.ifError(request.helloRequest)
t.assert.ok(request.helloRequest, "Hello World")
t.assert.ok(fastify.helloInstance, "Hello Fastify Instance")
return ({ message: request.helloRequest })
})
const fastifyResponse = await fastify.inject({
method: "GET",
url: "/"
})
t.assert.strictEqual(fastifyResponse.statusCode, 200)
t.assert.deepStrictEqual(JSON.parse(fastifyResponse.body), { message: "Hello World" })
})