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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 17:50:36