Mastering Fastify Application Testing with node:test in TypeScript
In this article, we'll explore native testing in Node.js, specifically utilizing the built-in node:test library for TypeScript-based Fastify applications. Drawing from my own experience, where I previously relied on third-party libraries like Jest, we'll delve into the simplicity and effectiveness of incorporating node:test directly into your testing toolkit. Join me in discovering a seamless testing approach tailored for TypeScript and Fastify development.
Let's start with a standard Fastify + Typescript App
src/app.ts
// ... imports ...
async function buildApp (options: Partial<typeof defaultOptions> = {}) {
const app: FastifyInstance = Fastify({ ...defaultOptions, ...options })
.withTypeProvider<TypeBoxTypeProvider>()
app.register(autoload, {
dir: join(__dirname, 'plugins')
})
app.register(autoload, {
dir: join(__dirname, 'routes'),
options: { prefix: '/api' }
})
return app
}
export default buildApp
This is a simple route:
src/routes/getDevices.ts
// ... imports ...
export default async function (
fastify: FastifyInstance,
_opts: FastifyPluginOptions
): Promise<void> {
fastify.get<{ Reply: GetDevicesResponseType }>(
'/device',
async (_request, _reply) => {
return await fastify.fetchDevices()
}
)
}
This is the use case as a Fastify plugin:
src/plugins/fetchDevices.ts
// ... imports ...
async function fetchDevicesPlugin (
fastify: FastifyInstance,
_opts: FastifyPluginOptions
): Promise<void> {
fastify.decorate('fetchDevices', ExternalDevice.fetchDevices)
}
export default fp(fetchDevicesPlugin)
This is a point where we simulate the reading of devices through an external system:
src/external/device.ts
// ... imports ...
const fetchDevices = async (): Promise<DeviceCollectionType> => {
return [
{
id: '1',
name: 'Device 1',
address: '10.0.0.1'
},
{
id: '2',
name: 'Device 2',
address: '10.0.0.2'
},
{ .... }
]
}
export default {
fetchDevices
}
Now, we want to write a test using the 'node:test' library, mocking the external dependency.
test/routes/getDevices.test.ts
import buildApp from '@src/app'
import { FastifyInstance } from 'fastify'
import { describe, it, after, mock } from 'node:test'
import assert from 'assert'
import ExternalDevice from '@src/external/device'
describe('GET /device HTTP', () => {
let app: FastifyInstance
const mockDevices = [
{
id: 'test',
name: 'Device Test',
address: '10.0.2.12'
}
]
const mockDevicesFn = mock.fn(async () => mockDevices)
const mockDevicesErrorFn = mock.fn(async () => {
throw new Error('Error retrieving devices')
})
after(async () => {
await app.close()
})
it('GET /device returns status 200', async () => {
mock.method(ExternalDevice, 'fetchDevices', mockDevicesFn)
app = await buildApp({ logger: false })
const response = await app.inject({
method: 'GET',
url: '/api/device'
})
assert.strictEqual(response.statusCode, 200)
assert.deepStrictEqual(JSON.parse(response.payload), mockDevices)
assert.strictEqual(mockDevicesFn.call.length, 1)
const call = mockDevicesFn.mock.calls[0]
assert.deepEqual(call.arguments, [])
mock.reset()
})
})
Now, we also want to test the scenario where reading from the external system fails, still using a mock.
// ...
it('GET /device returns status 500', async () => {
mock.method(ExternalDevice, 'fetchDevices', mockDevicesErrorFn)
app = await buildApp({ logger: false })
const response = await app.inject({
method: 'GET',
url: '/api/device'
})
const expectedResult = {
statusCode: 500,
error: 'Internal Server Error',
message: 'Error retrieving devices'
}
assert.strictEqual(response.statusCode, 500)
assert.deepStrictEqual(JSON.parse(response.payload), expectedResult)
assert.strictEqual(mockDevicesFn.call.length, 1)
const call = mockDevicesFn.mock.calls[0]
assert.deepEqual(call.arguments, [])
mock.reset()
})
// ...
How to set up the project the right way
We are using ts-node to utilize TypeScript.
package.json
{
"scripts": {
...
"test": "node -r ts-node/register --test test/**/*.test.ts",
"test:watch": "node -r ts-node/register --test --watch test/**/*.test.ts",
"test:coverage": "nyc pnpm run test",
...
},
...
}
tsconfig.json
{
"ts-node": {
"require": ["tsconfig-paths/register"],
"files": true
},
"extends": "fastify-tsconfig",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"baseUrl": "./",
"paths": {
"@src/*": ["src/*"],
"@test/*": ["test/*"],
},
"outDir": "./dist"
}
}
Launch Tests
pnpm run test
About coverage
The coverage function of node:test is still in an experimental phase. However, it is possible to achieve good results by using the nyc library in conjunction with node:test.
pnpm run test:coverage
Conclusions
The project code is in this GitHub repository: fastify-ts-nodetest.
The native node:test function serves as an excellent alternative to third-party libraries. While it is still evolving, it effectively fulfills its purpose. Developers can leverage its capabilities to replace external dependencies, benefitting from its evolving features and robust performance in testing scenarios.
Top comments (0)