使用Jest进行Node.js单元测试的实践指南

一. 为之前的文件模块todo list项目做测试

  • 先安装测试依赖jest yarn add --dev jest
  • 根据文档创建文件sum.jssum.test.js复制代码
js 复制代码
// sum.js
function sum(a, b) {
  return a + b;
}

module.exports = sum;

// sum.test.js
const sum = require('./sum')

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
})
  • 在package.json中添加脚本,之后运行yarn test
js 复制代码
  "scripts": {
    "test": "jest"
  },
  • 如果能得到以下图片的运行结果,则说明测试用例被正确执行了
  • 如果错了为怎么样见下图

二.正式开始为项目做测试

  • 主要测试db.js中的read和write
  • 添加__tests__文件夹,这是jest的约定
  • 在此文件夹下创建db.spec.js,这样命名是单元测试不成文的规定
  • 先写一个最基本的测试用例,期待db.read是一个函数
js 复制代码
const db = require('../db.js')

describe('db', fn => {
  it('can read', ()=>{
    expect(db.read instanceof Function).toBe(true)
  })
  it('can write', ()=>{})
})

三. 单元测试的写法

  • 一般单元测试是白盒测试,知道代码怎么写的,按照写的代码来测试
  • 如何测试读文件,初步想法先在某个路径创建一个文件,然后去读这个路径上的文件,如果能读出就表示正确
  • 但是这样子做会有问题,依赖了外部环境,比如此路径已经有了这个文件,就会产生bug
  • 单元测试不能与外界打交道,与外界打交道是集成测试应该做的
  • 我们通过jest.mock来做,新建__mocks__/fs.js,实现一个最简单的例子
js 复制代码
// __mocks__/fs.js
const fs = jest.genMockFromModule('fs')
fs.x = () => {
  console.log('hi')
  return 'xxx'
}
module.exports = fs;

// db.spec.js
const db = require('../db.js')
const fs = require('fs')
jest.mock('fs')

describe('db', fn => {
  it('can read', ()=>{
    expect(fs.x()).toBe('xxx')
  })
})
  • 测试读文件db.read,其实就是fs.read,所以需要在fs.read上做一些手脚
  • 最终测试db.read的代码
js 复制代码
// fs.js
// 这段代码是使用Jest测试框架中的jest.genMockFromModule函数来创建一个名为fs的模拟模块。在测试中,有时需要模拟一些Node.js核心模块,如文件系统(fs)模块,以便在测试过程中可以控制和检查模块的行为。
const fs = jest.genMockFromModule('fs')
// 这一行使用 jest.requireActual 获取实际的 fs 模块,保存在 _fs 变量中。这将允许模拟模块在需要时调用真实的 fs 模块功能。
const _fs = jest.requireActual('fs')

Object.assign(fs, _fs)

const mocks = {}

//  这个函数用于设置模拟的文件读取操作的期望结果。您可以通过调用 fs.setMock 来指定特定路径的文件读取应该返回的错误和数据。s
fs.setMock = (path, error, data) => {
  mocks[path] = [error, data]
}

// 这个函数被用来模拟 fs.readFile 函数。它接受文件路径、选项和回调函数,检查是否有为特定路径设置的模拟结果。如果有模拟结果,
// 它会调用回调函数,并传递预先设置的错误和数据。如果没有模拟结果,它将调用真实的 _fs.readFile 函数。
fs.readFile = (path, options, callback) => {
  if(callback === undefined) {
    callback = options
  }
  if(path in mocks) {
    callback(...mocks[path])
  } else {
    _fs.readFile(path, options, callback)
  }
}

module.exports = fs;
  • db.spec.js
js 复制代码
const db = require('../db.js')
const fs = require('fs')
// 这段代码是使用Jest测试框架中的jest.mock函数来模拟Node.js的文件系统(fs)模块。jest.mock函数用于模拟模块,以便在测试过程中控制和检查模块的行为,而不实际执行真实的模块代码。
jest.mock('fs')

describe('db', fn => {
  it('can read', async ()=>{
    const data = [{title: 'hi', done: true}]
    fs.setMock('/xxx', null, JSON.stringify(data))
    const list = await db.read('/xxx')
    expect(list).toStrictEqual(data)
  })
})

四. 测试写文件行为

  • 这里要注意,并非真正的去写一个文件,而是把文件写到任意路径
  • fs.js
js 复制代码
const writeMocks = {}

fs.setWriteFileMock = (path, fn) => {
  writeMocks[path] = fn
}

fs.writeFile = (path, data, options, callback) => {
  if (callback === undefined) {
    callback = options
  }

  if(path in writeMocks) {
    writeMocks[path](path, data, options, callback)
  } else {
    _fs.writeFile(path, data, options, callback)
  }
}
  • db.spec.js
js 复制代码
  it('can write', async () => {
    let fakeFile
    fs.setWriteFileMock('/yyy', (path, data, callback) => {
      fakeFile = data
      callback(null)
    })
    const list = [{title: '吃饭',done:true}, {title: '洗脚', done: true}]
    await db.write(list, '/yyy')
    expect(fakeFile).toBe(JSON.stringify(list))
  })

五. 增加clearMocks

  • 我们需要在每一个测试结束后清理测试数据,避免单元测试互相干扰
js 复制代码
// fs.js
fs.clearMocks = () => {
  readMocks = {}
  writeMocks = {}
}

// db.spec.js

afterEach(()=>{
  fs.clearMocks()
})

六. 最后总结

  • 如何测试

    • 选择测试工具:jest
    • 设计测试用例
    • 写测试,运行测试,改代码
  • 单元测试、功能测试、集成测试的区别

    • 单元测试不应该与外界打交道(那是集成测试要做的)
    • 单元测试的对象是函数
    • 功能测试的对象是模块
    • 集成测试的对象是系统
    • 一名优秀的开发者应该优先保证能做到单元测试
相关推荐
涡能增压发动积1 天前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
Wenweno0o1 天前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨1 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz1 天前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg3213211 天前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
从前慢丶1 天前
前端交互规范(Web 端)
前端
tyung1 天前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald1 天前
SpringBoot - 自动配置原理
java·spring boot·后端
CHU7290351 天前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing1 天前
Page-agent MCP结构
前端·人工智能