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); }) }
测试时出现两类稳定复现的异常:
- MobX抛出报错:
[MobX] 'makeAutoObservable' can only be used for classes that don't have a superclass - Mock的
TypesController.getTypeByName返回结果完全随机,多次运行相同用例可能返回null也可能返回TypeModel实例,无规律。
问题成因分析
MobX报错成因
该报错是装饰器转译配置差异+MobX校验规则共同导致的,本质是写法存在兼容隐患,只是生产环境碰巧绕过了校验:
- 按照TS类装饰器规范,
@WithType装饰类之后,会返回一个继承自被装饰类的匿名子类作为实际构造函数。以测试中的TestModel为例,实际实例化的类继承链为:装饰器生成的匿名类 -> TestModel -> InstanceModel -> Object。 - 生产环境构建时,Webpack链路的Babel/SWC一般默认开启类语法、装饰器的loose模式,会把ES6类转译为ES5构造函数,MobX 6+的
makeAutoObservable父类校验逻辑在ES5构造函数场景下无法正确识别多层自定义继承链,因此不会触发报错。 - Jest默认的babel-jest配置不会开启类语法的loose模式,转译后的ES6类保留了完整的原型链标记,当
InstanceModel构造函数中调用makeAutoObservable(this)时,MobX检测到当前this的构造函数(装饰器生成的匿名类)存在多层自定义父类,直接触发校验规则抛出错误。
Mock返回随机成因
三个问题叠加导致结果完全不可控:
- 模块级缓存污染:装饰器中使用的
cache是定义在装饰器模块顶层的全局缓存,Jest运行测试时默认会复用模块实例,只要测试进程中一次调用写入了缓存,后续所有实例化逻辑都不会再调用TypesController.getTypeByName,直接读取缓存值,预先配置的mockReturnValueOnce/mockReturnValue返回序列完全不生效。 - Mock执行时机错误:虽然Jest会把
jest.mock提升到文件顶部执行,但如果测试文件没有显式import装饰器模块、或者存在循环依赖,装饰器内部拿到的TypesController引用和mock的引用不是同一个,mock的调用规则根本不会作用到装饰器实际调用的方法上。 - 异步逻辑未等待:装饰器内的
setType是异步函数,Proxy拦截到typeName赋值时,会异步发起请求、异步更新type属性;而plainToInstance是同步执行的,同步执行完实例化之后立刻断言时,异步的setType大概率还没执行完成,type属性还是初始null值,断言结果完全取决于事件循环的执行时机,自然毫无规律。
内容的提问来源于stack exchange,提问作者Як Цидрак
相关产品推荐
相关产品推荐

