探索快速化和打字稿:模拟外部依赖关系
#typescript #node #测试 #fastify

介绍

在本文中,我将带您完成设置快速 +打字条项目的过程,并演示如何有效模拟外部依赖关系。我创建了一个演示应用程序来展示此概念。

项目设置

创建新的快速项目

mkdir fastify-ts-mock
cd fastify-ts-mock

npm init -y

安装打字稿

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

tsc --init

安装快速

npm i fastify fastify-plugin @fastify/autoload

文件夹结构

(root) fastify-ts-mock
    |-- src
         |-- lib
         |-- plugins
         |-- routes
         |-- type 
    |-- test

路径别名

npm i module-alias
npm i -D @types/module-alias tsconfig-paths

将以下行添加到tsconfig.json

{
  "ts-node": {
    "require": ["tsconfig-paths/register"]
  },
  ...
  "baseUrl": "./",
  "paths": {
      "@src/*": ["src/*"],
      "@type/*": ["src/type/*"],
      "@lib/*": ["src/lib/*"],
      "@test/*": ["test/*"],
   },     
   ...  
}

快速应用程序

文件: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
}

列出所有设备路线(/api/devices

文件: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()
      }
    },
  )
}

列出所有设备功能

文件: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)

此插件使用DeviceLib.listDevices,这是一个外部依赖。
为了正确测试路线,我需要对其进行模拟。

安装测试库

npm i tap
npm i -D @types/tap

测试代码(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 ...
})

第一种方法:用工厂装饰插件

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

专利:

  • 简单的方法
  • 不需要第三方图书馆

cons:

  • 更多插件复杂性=更多模拟功能复杂性
  • 维护的代码
  • 不是错误的代码

更好的方法:使用ImportMock

安装依赖项:

npm i -D sinon ts-mock-imports

更新测试代码(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)
})

专利:

  • 非常简单
  • 易于维护

cons:

  • 需要第三方库

我们不是在模拟快速插件,而是在插件中使用的库。

如果我们尝试模拟插件怎么办?

const listDevicesMock = ImportMock.mockFunction(
    app,
    'listDevices',
    Fixtures.devices
)

它不起作用:我们得到此错误:

Cannot stub non-existent property listDevices

结论

简而言之,以正确的方式写作测试不仅重要 - 至关重要。适当的测试确保我们的代码按预期工作并保持可靠。因此,请记住,好的测试意味着很棒的软件!

随时查看repository的动手体验,并更深入地了解该过程。愉快的编码!