DEV Community

Cover image for Testing Fastify with Node:Test
Massimo Biagioli for Claranet

Posted on • Updated on

Testing Fastify with Node:Test

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

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

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

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

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()
  })

})
Enter fullscreen mode Exit fullscreen mode

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

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

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

Launch Tests

pnpm run test
Enter fullscreen mode Exit fullscreen mode

Image description

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

Image description

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)