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

Jest测试带装饰器与Proxy的MobX类报错问题求解

问题背景

开发与生产环境运行正常的MobX自定义装饰器逻辑,在Jest单元测试场景下持续抛出两类异常,相关实现如下:

基础代码实现

  • 模块1:基础类型模型
export class TypeModel {
  public id: string
  public parameters: any[]
}
  • 模块2:MobX可观察基类
export class InstanceModel {
  public typeName: string

  constructor() {
    makeAutoObservable(this)
  }
}
  • 模块3:自定义@WithType装饰器
export function WithType({
  source = 'typeName',
  destination = 'type',
  makeItObservable = false
}: WithTypeOptions) {
    return function<T extends Constructor>(Class: T): T {
      return class extends Class {
        constructor(...args: any[]) {
          super(...args);

          /** 定义新增属性 */
          Reflect.defineProperty(this, destination, { 
            enumerable: true, 
            configurable: true, 
            writable: true, 
            value: null 
          })

          /** 给新增属性加MobX响应式 */
          if (makeItObservable) {
            extendObservable(this, { [destination]: null })
          }

          const setProperty = (value: TypeModel | null) => {
            Reflect.set(this, destination, value)
          }

          const setType = async (name: string) => {
            /** 用缓存减少网络请求,类型数据基本不会变更 */
            if (!cache.hasOwnProperty(name)) {
              const type = await TypesController.getTypeByName(name)
              if (!type) {
                console.error(`In WithType > setType: Type ${name} wasn't found`)
              }
              cache[name] = type
            }

            /** 从缓存取类型赋值 */
            setProperty(plainToInstance(TypeModel, cache[name])) 
          }

          /** 用Proxy监听源属性变更 */
          return new Proxy(this, {
            set: (target: Record<string | symbol, any>, key: string | symbol, value: any) => {
              // 先设置原始值
              target[key] = value
              
              // 仅当监听的源属性变更时触发逻辑
              if (key !== source) return true

              // 执行异步类型拉取
              setType(value).catch(e => console.log('In @WithType ', e))
              
              return true
            }
          })
        }
      };
    };
}
  • 模块4:装饰器应用类
@WithType({ makeItObservable: true })
export class FinalModel extends InstanceModel {}
  • 生产环境实例化逻辑
api.get(json => {
  const model = plainToInstance(FinalModel, json)
})

以上逻辑在开发、生产环境均运行正常:生成的实例是MobX响应式对象,包含装饰器新增的可观察属性。

测试代码与异常现象

测试代码如下:

/** 装饰器依赖的TypesController是webpack module-federation提供的模块,mock为虚拟模块 */
jest.mock('shell/typesController',
  () => ({
    __esModule: true,
    TypesController: {
      getTypeByName: jest.fn()
        .mockReturnValue(null)
        .mockReturnValueOnce(mockedType())
    },
  }), { virtual: true }
)


describe('@WithType',  () => {
  it('', async () => {

    @WithType({ makeItObservable: true })
    class TestModel extends InstanceModel {}
    interface TestModel extends WithType {}

    const model: TestModel = plainToInstance(TestModel, mockInstance());
    expect(model.type).toBeInstanceOf(TypeModel) 

    const model1 = plainToInstance(TestModel, mockInstance());
    expect(model1).toHaveProperty('type', null);

  })
}

测试时出现两类稳定复现的异常:

  1. MobX抛出报错:[MobX] 'makeAutoObservable' can only be used for classes that don't have a superclass
  2. Mock的TypesController.getTypeByName返回结果完全随机,多次运行相同用例可能返回null也可能返回TypeModel实例,无规律。
问题成因分析

MobX报错成因

该报错是装饰器转译配置差异+MobX校验规则共同导致的,本质是写法存在兼容隐患,只是生产环境碰巧绕过了校验:

  1. 按照TS类装饰器规范,@WithType装饰类之后,会返回一个继承自被装饰类的匿名子类作为实际构造函数。以测试中的TestModel为例,实际实例化的类继承链为:装饰器生成的匿名类 -> TestModel -> InstanceModel -> Object。
  2. 生产环境构建时,Webpack链路的Babel/SWC一般默认开启类语法、装饰器的loose模式,会把ES6类转译为ES5构造函数,MobX 6+的makeAutoObservable父类校验逻辑在ES5构造函数场景下无法正确识别多层自定义继承链,因此不会触发报错。
  3. Jest默认的babel-jest配置不会开启类语法的loose模式,转译后的ES6类保留了完整的原型链标记,当InstanceModel构造函数中调用makeAutoObservable(this)时,MobX检测到当前this的构造函数(装饰器生成的匿名类)存在多层自定义父类,直接触发校验规则抛出错误。

Mock返回随机成因

三个问题叠加导致结果完全不可控:

  1. 模块级缓存污染:装饰器中使用的cache是定义在装饰器模块顶层的全局缓存,Jest运行测试时默认会复用模块实例,只要测试进程中一次调用写入了缓存,后续所有实例化逻辑都不会再调用TypesController.getTypeByName,直接读取缓存值,预先配置的mockReturnValueOnce/mockReturnValue返回序列完全不生效。
  2. Mock执行时机错误:虽然Jest会把jest.mock提升到文件顶部执行,但如果测试文件没有显式import装饰器模块、或者存在循环依赖,装饰器内部拿到的TypesController引用和mock的引用不是同一个,mock的调用规则根本不会作用到装饰器实际调用的方法上。
  3. 异步逻辑未等待:装饰器内的setType是异步函数,Proxy拦截到typeName赋值时,会异步发起请求、异步更新type属性;而plainToInstance是同步执行的,同步执行完实例化之后立刻断言时,异步的setType大概率还没执行完成,type属性还是初始null值,断言结果完全取决于事件循环的执行时机,自然毫无规律。

内容的提问来源于stack exchange,提问作者Як Цидрак

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 19:12:29