MobX-State-Tree测试异常:Mock动作触发断言问题与MST保护错误
解决MobX-State-Tree(MST)组件测试中Mock动作方法的问题
核心问题拆解
- 直接用
vi.fn()替换MST动作会破坏它的内部约束,导致断言完全失效(假阳性) - 组件的
useEffect是异步执行的,直接断言会因为时序问题拿不到调用结果 vi.spyOn直接修改MST实例方法违反了它的保护机制——MST要求动作必须通过自身的action包装修改实例
正确解决方案
方案1:用MST自带的onAction钩子监听动作(最推荐)
这是最贴合MST设计的方式,不需要修改实例,通过钩子捕获动作执行:
import { render, waitFor } from '@testing-library/react'; import { onAction } from 'mobx-state-tree'; import SampleStore from './SampleStore'; import SampleComponent from './SampleComponent'; test('组件挂载时调用sample_action', async () => { const store = SampleStore.create({}); let actionCallCount = 0; // 监听指定动作 const disposer = onAction(store, (call) => { if (call.name === 'sample_action') { actionCallCount++; } }); render(<SampleComponent store={store} />); // 等待useEffect异步执行完成 await waitFor(() => { expect(actionCallCount).toBe(1); }); // 清理监听,避免影响其他测试 disposer(); });
方案2:在MST模型中预留测试Mock入口
如果需要更灵活的Mock逻辑,可以在模型定义时加一个测试专用的替换方法:
// SampleStore.ts import { types } from 'mobx-state-tree'; const SampleStore = types .model('SampleStore', {}) .actions((self) => { // 真实业务逻辑的动作 const realSampleAction = () => { // 你的业务代码 }; // 可替换的动作变量,默认用真实实现 let sample_action = realSampleAction; // 测试专用:替换动作的方法 const __setSampleActionMock = (mockFn: () => void) => { sample_action = mockFn; }; return { sample_action, __setSampleActionMock // 仅在测试环境使用 }; }); export default SampleStore;
测试时直接调用预留方法设置Mock:
import { render, waitFor } from '@testing-library/react'; import vi from 'vitest'; import SampleStore from './SampleStore'; import SampleComponent from './SampleComponent'; test('组件挂载时调用sample_action', async () => { const store = SampleStore.create({}); const mockAction = vi.fn(); // 设置Mock动作 store.__setSampleActionMock(mockAction); render(<SampleComponent store={store} />); await waitFor(() => { expect(mockAction).toHaveBeenCalledTimes(1); }); });
方案3:用MobX全局spy监听
不想修改模型的话,也可以用MobX的全局spy功能捕获动作:
import { render, waitFor } from '@testing-library/react'; import { spy } from 'mobx'; import SampleStore from './SampleStore'; import SampleComponent from './SampleComponent'; test('组件挂载时调用sample_action', async () => { const store = SampleStore.create({}); const actionSpy = spy((event) => { if (event.type === 'action' && event.name === 'sample_action' && event.target === store) { actionSpy.callCount++; } }); actionSpy.callCount = 0; render(<SampleComponent store={store} />); await waitFor(() => { expect(actionSpy.callCount).toBe(1); }); // 清理监听 actionSpy.dispose(); });
关键注意点
- 必须用
waitFor等待useEffect的异步执行完成,否则会因为时序问题断言失败 - 绝对不要直接修改MST实例的方法,必须通过它允许的方式(钩子、预留入口)来监听或替换动作
- 测试结束后一定要清理监听/钩子,避免污染其他测试用例
内容的提问来源于stack exchange,提问作者Airah
相关产品推荐
相关产品推荐

