Testing Fastify with Node:Test
Published: 2024-01-05 10:18:11

## 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`

```typescript
// ... 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`

```typescript
// ... 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`

```typescript
// ... 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`

```typescript
// ... 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`

```typescript
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.

```typescript
// ...
  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`

```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`

```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

```bash
pnpm run test
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1molyq9brri8obuq5pwt.png)

### 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.

```bash
pnpm run test:coverage
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/owu48ak38ef9a2hd3sml.png)

## Conclusions
The project code is in this GitHub repository: [fastify-ts-nodetest](https://github.com/massimobiagioli/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.