You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js测试:不修改控制器Mock/Spy内部employees集合

问题背景

我有一个employees.js控制器,内部维护了未导出的employees集合,还有对应的测试文件employees.test.js。目前的测试是通过mock控制器方法来实现自定义逻辑,根本没验证原控制器的真实业务逻辑。需要在不修改原控制器代码(不能导出内部的employees集合)的前提下,修复测试,让它能验证真实逻辑。

原控制器代码:

let employees = [
  { id: '1', name: "Edmon D'antes", status: 'worker' },
  { id: '2', name: 'Alluri Sitarama Raju', status: 'hero' },
  { id: '3', name: 'Roman Shukhevych', status: 'hero' },
  { id: '4', name: 'Ptn Pnh', status: 'huylo' },
]

export const getAll = (req, res) => {
  res.status(200).json(employees)
}

export const create = (req, res) => {
  const newEmployee = {
    id: Date.now().toString(),
    ...req.body,
  }
  employees.push(newEmployee)
  res.status(201).json(newEmployee)
}

export const remove = (req, res) => {
  let message = 'Employee has been dismissed.'
  employees = employees.filter((e) => {
    if (e.id === req.params.id && e.status === 'hero') {
      message = 'You must respect heroes!'
      return true
    }
    return e.id !== req.params.id
  })
  res.json({ message })
}

原测试代码:

import * as employeesModule from './employees.js'

let mockEmployees = [
  { id: '1', name: "Mocked Edmon D'antes", status: 'worker' },
  { id: '2', name: 'Mocked Alluri Sitarama Raju', status: 'hero' },
  { id: '3', name: 'Mocked Roman Shukhevych', status: 'hero' },
  { id: '4', name: 'Mocked Ptn Pnh', status: 'huylo' },
]

beforeEach(() => {
  jest.spyOn(employeesModule, 'getAll').mockImplementation((req, res) => {
    return res.status(200).json(mockEmployees)
  })
  jest.spyOn(employeesModule, 'create').mockImplementation((req, res) => {
    const newEmployee = {
      id: '5',
      ...req.body,
    }
    mockEmployees.push(newEmployee)
    return res.status(201).json(newEmployee)
  })
  jest.spyOn(employeesModule, 'remove').mockImplementation((req, res) => {
    let message = 'Employee has been dismissed.'
    mockEmployees = mockEmployees.filter((e) => {
      if (e.id === req.params.id && e.status === 'hero') {
        message = 'You must respect heroes!'
        return true
      }
      return e.id !== req.params.id
    })
    return res.json({ message })
  })
})

describe('employees controller', () => {
  it('getAll method should return all employees', () => {
    const req = {} // Provide an empty object for req
    const res = { status: jest.fn().mockReturnThis(), json: jest.fn() }
    employeesModule.getAll(req, res)
    expect(res.status).toHaveBeenCalledWith(200)
    expect(res.json).toHaveBeenCalledWith(mockEmployees)
  })

  it('create method should create a new employee', () => {
    const req = { body: { name: 'New Employee', status: 'working' } }
    const res = { status: jest.fn().mockReturnThis(), json: jest.fn() }
    employeesModule.create(req, res)
    const newEmployee = mockEmployees.find((e) => e.id === '5')
    expect(res.status).toHaveBeenCalledWith(201)
    expect(res.json).toHaveBeenCalledWith(newEmployee)
  })

  it('remove method should remove the employee if not a hero', () => {
    const req = { params: { id: '4' } }
    const res = { json: jest.fn() }
    employeesModule.remove(req, res)
    expect(res.json).toHaveBeenCalledWith({
      message: 'Employee has been dismissed.',
    })
  })

  it('remove method should not remove the employee if a hero', () => {
    const req = { params: { id: '2' } }
    const res = { json: jest.fn() }
    employeesModule.remove(req, res)
    expect(res.json).toHaveBeenCalledWith({
      message: 'You must respect heroes!',
    })
  })
})
解决方案

不用改原控制器代码,直接用Jest的模块模拟API里的__get__和__set__方法,就能直接操作原模块内部的私有employees变量,同时调用真实的控制器方法,通过检查集合变化和响应来验证逻辑。

修改后的测试代码如下:

import * as employeesModule from './employees.js'

// 定义测试用的基础员工数据,每个测试用例都会基于这个初始化
const testEmployees = [
  { id: '1', name: "Edmon D'antes", status: 'worker' },
  { id: '2', name: 'Alluri Sitarama Raju', status: 'hero' },
  { id: '3', name: 'Roman Shukhevych', status: 'hero' },
  { id: '4', name: 'Ptn Pnh', status: 'huylo' },
]

beforeEach(() => {
  // 重置原模块的employees集合为测试初始数据,避免用例间污染
  employeesModule.__set__('employees', [...testEmployees])
  
  // 清除所有方法的mock,确保调用的是控制器的真实逻辑
  jest.restoreAllMocks()
})

describe('employees controller', () => {
  it('getAll should return all employees with 200 status', () => {
    const req = {}
    const res = { status: jest.fn().mockReturnThis(), json: jest.fn() }
    
    employeesModule.getAll(req, res)
    
    expect(res.status).toHaveBeenCalledWith(200)
    expect(res.json).toHaveBeenCalledWith(testEmployees)
  })

  it('create should add a new employee and return it with 201 status', () => {
    const req = { body: { name: 'New Employee', status: 'worker' } }
    const res = { status: jest.fn().mockReturnThis(), json: jest.fn() }
    
    // Mock Date.now()避免生成随机id,保证断言稳定
    jest.spyOn(Date, 'now').mockReturnValue(123456789)
    
    employeesModule.create(req, res)
    
    // 获取原模块当前的employees集合,验证是否新增成功
    const currentEmployees = employeesModule.__get__('employees')
    
    expect(res.status).toHaveBeenCalledWith(201)
    expect(res.json).toHaveBeenCalledWith({
      id: '123456789',
      name: 'New Employee',
      status: 'worker'
    })
    expect(currentEmployees.length).toBe(testEmployees.length + 1)
    expect(currentEmployees).toContainEqual({
      id: '123456789',
      name: 'New Employee',
      status: 'worker'
    })
  })

  it('remove should dismiss non-hero employee and update collection', () => {
    const req = { params: { id: '4' } }
    const res = { json: jest.fn() }
    
    employeesModule.remove(req, res)
    
    const currentEmployees = employeesModule.__get__('employees')
    
    expect(res.json).toHaveBeenCalledWith({
      message: 'Employee has been dismissed.'
    })
    expect(currentEmployees.length).toBe(testEmployees.length - 1)
    expect(currentEmployees.find(e => e.id === '4')).toBeUndefined()
  })

  it('remove should not dismiss hero employee and keep collection unchanged', () => {
    const req = { params: { id: '2' } }
    const res = { json: jest.fn() }
    
    employeesModule.remove(req, res)
    
    const currentEmployees = employeesModule.__get__('employees')
    
    expect(res.json).toHaveBeenCalledWith({
      message: 'You must respect heroes!'
    })
    expect(currentEmployees.length).toBe(testEmployees.length)
    expect(currentEmployees.find(e => e.id === '2')).toBeDefined()
  })
})

关键说明

  • __get__/__set__用法:Jest在处理模块时会自动给模块对象添加这两个方法,专门用来访问和修改模块内部的私有变量,不需要原代码导出employees。
  • 测试环境隔离:每个测试用例执行前都会重置employees为初始测试数据,同时恢复所有mock,保证用例之间互不干扰。
  • 处理随机依赖:对Date.now()做mock,避免创建员工时id的随机性导致断言失败,让测试结果稳定。
  • 验证真实逻辑:不再mock控制器的方法,直接调用真实代码,通过检查响应内容和内部集合的变化,确保业务逻辑符合预期。

内容的提问来源于stack exchange,提问作者Serhii

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 23:14:54