DEV Community

Cover image for Exploring Fastify and TypeScript: Mocking External Dependencies
Massimo Biagioli for Claranet

Posted on • Edited on

Exploring Fastify and TypeScript: Mocking External Dependencies

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
Enter fullscreen mode Exit fullscreen mode

Install Typescript

npm i -D typescript @types/node ts-node

tsc --init
Enter fullscreen mode Exit fullscreen mode

Install Fastify

npm i fastify fastify-plugin @fastify/autoload
Enter fullscreen mode Exit fullscreen mode

Folder structure

(root) fastify-ts-mock
    |-- src
         |-- lib
         |-- plugins
         |-- routes
         |-- type 
    |-- test
Enter fullscreen mode Exit fullscreen mode

Path alias

npm i module-alias
npm i -D @types/module-alias tsconfig-paths
Enter fullscreen mode Exit fullscreen mode

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/*"],
   },     
   ...  
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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()
      }
    },
  )
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ...
})
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

It doesn't work: we get this error:

Cannot stub non-existent property listDevices
Enter fullscreen mode Exit fullscreen mode

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)