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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 16:25:39