使用Sinon、Mocha和SWC存根导出函数失败
用SWC转译时,Sinon无法Stub ES模块导出的函数(报错:TypeError: Descriptor for property xxx is non-configurable and non-writable)
我通过import * as导入ES模块,并用Sinon对模块导出的函数做Stub,在ts-node转译(命令:mocha --require ts-node/register */**/*.spec.ts)时能正常工作,但换成SWC转译(命令:mocha --require @swc/register */**/*.spec.ts)就报错:
TypeError: Descriptor for property validate is non-configurable and non-writable
相关代码示例
hash.ts
import * as argon2 from 'argon2' export async function encrypt(plain) { return await argon2.hash(plain) } export async function validate(hash, plain) { return await argon2.verify(hash, plain) }
service.ts
import { validate } from './hash' export async function isValidUser(user: User, password: string) { if (!user || !(await validate(user.password, password))) { return false } return true }
service.spec.ts
import * as hash from './hash' import { isValidUser } from './service' import { stub } from 'sinon' describe('isValidUser', () => { stub(hash, 'validate').callsFake( async (passwordFormDB, passwordFromUserInput) => passwordFormDB === passwordFromUserInput ) it('...', async () => { /* test `isValidUser` function */ }) })
.swcrc配置
{ "test": ".*.ts$", "jsc": { "parser": { "syntax": "typescript", "decorators": true, "importMeta": true }, "transform": { "legacyDecorator": true, "decoratorMetadata": true }, "paths": { "src/*": ["./src/*"] } }, "module": { "type": "commonjs", "noInterop": true } }
问题原因
SWC将ES模块转译为CommonJS时,默认会把导出的函数设置为不可配置、不可写的属性,而Sinon的stub需要修改目标属性的可写性来替换函数,两者产生冲突。而ts-node转译后的模块导出属性是可配置可写的,因此能正常Stub。
解决方案
方案1:修改SWC配置,放开导出属性的可写限制
在.swcrc的module配置中添加strict: false,让SWC转译时不强制把导出函数设为不可写:
{ "test": ".*.ts$", "jsc": { "parser": { "syntax": "typescript", "decorators": true, "importMeta": true }, "transform": { "legacyDecorator": true, "decoratorMetadata": true }, "paths": { "src/*": ["./src/*"] } }, "module": { "type": "commonjs", "noInterop": true, "strict": false // 添加此行 } }
方案2:改用依赖注入模式
调整业务代码结构,让isValidUser接收validate函数作为可选参数,避免直接依赖模块导出,Stub时直接传递模拟函数即可:
修改service.ts:
import { validate } from './hash' export async function isValidUser(user: User, password: string, validateFn = validate) { if (!user || !(await validateFn(user.password, password))) { return false } return true }
修改测试文件:
import { isValidUser } from './service' import { stub } from 'sinon' describe('isValidUser', () => { const validateStub = stub().callsFake( async (passwordFormDB, passwordFromUserInput) => passwordFormDB === passwordFromUserInput ) it('...', async () => { // 测试时传入stub函数 await isValidUser(userMock, 'test123', validateStub) // 编写断言逻辑 }) })
方案3:使用Sinon模块Stub工具
借助Sinon的createStubInstance或代理整个模块(需结合模块替换工具),不过这种方式需要调整导入和模块依赖关系,适合复杂场景。
总结
- 方案1改动最小,适合快速解决问题;
- 方案2的依赖注入模式更利于代码可维护性和可测试性,推荐长期采用。
内容的提问来源于stack exchange,提问作者Jaemin Choi
相关产品推荐
相关产品推荐

