Intro
In this article, I'll walk you through the process of setting up a Fastify + TypeScript project and demonstrate how to effectively mock external dependencies. I've created a demo application to showcase this concept.
Project Setup
Create new Fastify Project
mkdir fastify-ts-mock
cd fastify-ts-mock
npm init -y
Install Typescript
npm i -D typescript @types/node ts-node
tsc --init
Install Fastify
npm i fastify fastify-plugin @fastify/autoload
Folder structure
(root) fastify-ts-mock
|-- src
|-- lib
|-- plugins
|-- routes
|-- type
|-- test
Path alias
npm i module-alias
npm i -D @types/module-alias tsconfig-paths
Add the following lines to tsconfig.json
{
"ts-node": {
"require": ["tsconfig-paths/register"]
},
...
"baseUrl": "./",
"paths": {
"@src/*": ["src/*"],
"@type/*": ["src/type/*"],
"@lib/*": ["src/lib/*"],
"@test/*": ["test/*"],
},
...
}
Fastify App
File: src/app.ts
import 'module-alias/register';
import fastify, { FastifyInstance, FastifyServerOptions } from 'fastify'
import autoload from '@fastify/autoload'
import { join } from 'path'
export default function createApp(
opts?: FastifyServerOptions,
): FastifyInstance {
const defaultOptions = {
logger: true,
}
const app = fastify({ ...defaultOptions, ...opts })
app.register(autoload, {
dir: join(__dirname, 'plugins'),
})
app.register(autoload, {
dir: join(__dirname, 'routes'),
options: { prefix: '/api' },
})
return app
}
List all devices Route (/api/devices
)
File: src/routes/devices/list.ts
import { FastifyInstance, FastifyPluginOptions } from 'fastify'
import {DeviceDtoCollectionType} from "@type/devices.type";
export default async function (
fastify: FastifyInstance,
_opts: FastifyPluginOptions,
): Promise<void> {
fastify.get<{ Reply: DeviceDtoCollectionType }>(
'/',
async (request, reply) => {
try {
const devices = await fastify.listDevices()
return reply.send(devices)
} catch (error) {
request.log.error(error)
return reply.code(500).send()
}
},
)
}
List all devices Feature
File: src/plugins/features/devices.list.feature.ts
import fp from 'fastify-plugin'
import { FastifyInstance, FastifyPluginOptions } from 'fastify'
import * as DeviceLib from '@lib/devices.lib'
import {DeviceDtoCollectionType} from "@type/devices.type";
declare module 'fastify' {
interface FastifyInstance {
listDevices: () => Promise<DeviceDtoCollectionType>
}
}
async function listDevicesPlugin(
fastify: FastifyInstance,
_opts: FastifyPluginOptions,
): Promise<void> {
const listDevices = async (): Promise<DeviceDtoCollectionType> => DeviceLib.listDevices()
fastify.decorate('listDevices', listDevices)
}
export default fp(listDevicesPlugin)
This plugin use DeviceLib.listDevices
, which is an external dependency.
In order to properly test the route, I need to mock it.
Install test libraries
npm i tap
npm i -D @types/tap
Test code (test/routes/devices/list.test.ts
)
import {afterEach, test} from "tap"
import createApp from "@src/app";
import * as DeviceLib from '@lib/devices.lib'
import * as Fixtures from '@test/fixtures'
import {DeviceDtoCollectionType} from "@type/devices.type";
test('get all devices', async t => {
const app = createApp({
logger: false,
})
t.teardown(() => {
app.close();
})
const response = await app.inject({
method: 'GET',
url: '/api/devices',
})
const deviceCollection = response.json<DeviceDtoCollectionType>()
t.equal(response.statusCode, 200)
t.equal(deviceCollection.length, 2)
... other test assertion ...
})
First approach: decorate the plugin with a factory
async function listDevicesPlugin(
fastify: FastifyInstance,
_opts: FastifyPluginOptions,
): Promise<void> {
const listDevices = async (): Promise<DeviceDtoCollectionType> => {
return await DeviceLib.listDevices()
}
const listDevicesTest = async (): Promise<DeviceDtoCollectionType> => {
return [
... some fake data ...
]
}
function listDevicesFactory() {
if (fastify.config.ENV === 'test') {
return listDevicesTest()
}
return listDevices()
}
fastify.decorate('listDevices', listDevices)
}
PROS:
- easy approach
- no third-party libraries are needed
CONS:
- more plugin complexity = more mock function complexity
- code to maintain
- code that is not bug-free
A better approach: using ImportMock
Install dependencies:
npm i -D sinon ts-mock-imports
Update test code (test/routes/devices/list.test.ts
):
import {afterEach, test} from "tap"
import createApp from "@src/app";
import {ImportMock} from "ts-mock-imports";
import * as DeviceLib from '@lib/devices.lib'
import * as Fixtures from '@test/fixtures'
import {DeviceDtoCollectionType} from "@type/devices.type";
afterEach(() => {
ImportMock.restore();
})
test('get all devices', async t => {
const app = createApp({
logger: false,
})
t.teardown(() => {
app.close();
})
const listDevicesMock = ImportMock.mockFunction(
DeviceLib,
'listDevices',
Fixtures.devices
)
const response = await app.inject({
method: 'GET',
url: '/api/devices',
})
const deviceCollection = response.json<DeviceDtoCollectionType>()
t.equal(response.statusCode, 200)
t.equal(deviceCollection.length, 2)
... some data assertions ...
t.ok(listDevicesMock.calledOnce)
})
PROS:
- very simple
- easy to maintain
CONS:
- third-party libraries are needed
We are not mocking a fastify plugin, but a library used inside the plugin.
What if we try to mock the plugin instead?
const listDevicesMock = ImportMock.mockFunction(
app,
'listDevices',
Fixtures.devices
)
It doesn't work: we get this error:
Cannot stub non-existent property listDevices
Conclusion
In a nutshell, writing tests the right way isn't just important – it's crucial. Proper tests ensure our code works as intended and stays reliable. So, let's remember, good tests mean great software!
Feel free to check out the repository for a hands-on experience and deeper insights into the process. Happy coding!
Top comments (0)