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
相关产品推荐
相关产品推荐

