React中MobX Store的Jest独立单元测试方案咨询
Jest独立测试MobX Store的最佳方案
先解决你遇到的构造函数报错问题
你碰到TypeError: _EpochManager.default is not a constructor的核心原因是:你的代码默认导出的是React Context对象,而非EpochManager类本身。原代码最后一行是:
export default createContext(new EpochManager())
当你在测试中import EpochManager from './EpochManager'时,拿到的是React.Context实例,不是构造函数。
解决方法:修改Store代码,同时导出EpochManager类和Context:
// 导出类供测试使用 export class EpochManager { // 原类代码不变 } // 保留原Context导出供业务代码使用 const unirepConfig = (UnirepContext as any)._currentValue const userContext = (UserContext as any)._currentValue export default createContext(new EpochManager(unirepConfig, userContext))
测试时改为导入类:
import { EpochManager } from './EpochManager'
独立测试MobX Store的核心步骤
1. 重构Store,实现依赖注入
原代码直接硬编码读取UnirepContext和UserContext的_currentValue,这种写法无法Mock,是测试的大忌。重构思路是把外部依赖通过构造函数传入:
import { createContext } from 'react' import UnirepContext from './Unirep' import UserContext from './User' import { makeAutoObservable } from 'mobx' export class EpochManager { private timer: NodeJS.Timeout | null = null private currentEpoch = 0 readonly nextTransition = 0 readyToTransition = false // 用实例属性存储依赖 private unirepConfig: any private userContext: any // 构造函数接收依赖参数 constructor(unirepConfig: any, userContext: any) { makeAutoObservable(this) this.unirepConfig = unirepConfig this.userContext = userContext if (typeof window !== 'undefined') { this.updateWatch() } } async updateWatch() { // 替换为实例依赖 await this.unirepConfig.loadingPromise if (this.timer) { clearTimeout(this.timer) this.timer = null } this.readyToTransition = false this.currentEpoch = await this.unirepConfig.currentEpoch() ;(this as any).nextTransition = await this._nextTransition() const waitTime = Math.max(this.nextTransition - +new Date(), 0) console.log( `Next epoch transition in ${waitTime / (60 * 60 * 1000)} hours` ) this.timer = setTimeout(() => { this.timer = null this.tryTransition() }, waitTime) return waitTime } private async _nextTransition() { await this.unirepConfig.loadingPromise const [lastTransition, epochLength] = await Promise.all([ this.unirepConfig.unirep.latestEpochTransitionTime(), this.unirepConfig.epochLength, ]) return (lastTransition.toNumber() + epochLength) * 1000 } private async tryTransition() { for (;;) { const newEpoch = await this.userContext.loadCurrentEpoch() if (newEpoch > this.currentEpoch) { this.currentEpoch = newEpoch this.readyToTransition = true return } await new Promise((r) => setTimeout(r, 10000)) } } } // 业务代码依然用Context获取实例 const unirepConfig = (UnirepContext as any)._currentValue const userContext = (UserContext as any)._currentValue export default createContext(new EpochManager(unirepConfig, userContext))
2. 编写Jest测试用例
下面是针对EpochManager的核心测试示例,包含Mock依赖、异步处理、定时器模拟:
import { EpochManager } from './EpochManager' import { makeAutoObservable } from 'mobx' // Mock MobX的makeAutoObservable(可选,若不需要测试响应式细节) jest.mock('mobx', () => ({ makeAutoObservable: jest.fn((obj) => obj) })) describe('EpochManager', () => { let mockUnirepConfig: any let mockUserContext: any let epochManager: EpochManager beforeEach(() => { // 重置Jest定时器 jest.useFakeTimers() jest.clearAllMocks() // Mock UnirepConfig依赖 mockUnirepConfig = { loadingPromise: Promise.resolve(), currentEpoch: jest.fn().mockResolvedValue(1), epochLength: 3600, // 1小时秒数 unirep: { latestEpochTransitionTime: jest.fn().mockResolvedValue({ toNumber: () => Date.now() / 1000 - 1800 // 半小时前的时间戳 }) } } // Mock UserContext依赖 mockUserContext = { loadCurrentEpoch: jest.fn() } // 实例化Store,传入Mock依赖 epochManager = new EpochManager(mockUnirepConfig, mockUserContext) }) afterEach(() => { jest.useRealTimers() }) test('updateWatch 初始化后正确设置当前纪元和过渡时间', async () => { await epochManager.updateWatch() // 验证currentEpoch被正确设置 expect(epochManager['currentEpoch']).toBe(1) // 验证nextTransition计算正确(半小时前+1小时=半小时后,转成毫秒) expect(epochManager['nextTransition']).toBeCloseTo(Date.now() + 1800 * 1000, -3) // 验证定时器被创建 expect(setTimeout).toHaveBeenCalledTimes(1) }) test('tryTransition 检测到纪元变化后更新状态', async () => { // 初始currentEpoch设为1 epochManager['currentEpoch'] = 1 // 第一次调用返回1,第二次返回2 mockUserContext.loadCurrentEpoch.mockResolvedValueOnce(1).mockResolvedValueOnce(2) // 启动tryTransition并立即推进定时器 const tryTransitionPromise = epochManager['tryTransition']() jest.advanceTimersByTime(10000) await tryTransitionPromise // 验证状态更新 expect(epochManager['currentEpoch']).toBe(2) expect(epochManager.readyToTransition).toBe(true) // 验证loadCurrentEpoch被调用两次 expect(mockUserContext.loadCurrentEpoch).toHaveBeenCalledTimes(2) }) })
3. 关键测试技巧
- Mock外部依赖:所有与外部服务、Context、第三方库的交互都要Mock,确保测试只关注Store本身的逻辑。
- 处理异步逻辑:用
async/await等待异步方法完成,避免测试提前结束。 - 模拟定时器:使用
jest.useFakeTimers()替代真实定时器,用jest.advanceTimersByTime()或jest.runAllTimers()控制时间流逝,避免测试等待真实时间。 - 访问私有属性:测试时可以通过
epochManager['currentEpoch']访问私有属性,这在单元测试中是可接受的,因为我们需要验证内部状态变化。
内容的提问来源于stack exchange,提问作者AAMCODE
相关产品推荐
相关产品推荐

