DEV Community

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

Posted on • Edited on

6

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!

Image of Datadog

The Essential Toolkit for Front-end Developers

Take a user-centric approach to front-end monitoring that evolves alongside increasingly complex frameworks and single-page applications.

Get The Kit

Top comments (0)

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up