redux-observable与redux-mock-store联用出现重复动作流问题
我刚看完你的问题描述和复现场景,这个问题其实是redux-mock-store和redux-observable结合时的一个常见陷阱——每次调用mockStore()都会给epic的动作流新增一个订阅,而这些订阅不会自动清理,导致后续dispatch的动作被所有历史订阅接收,出现重复触发的情况。
问题根源
redux-mock-store的内部实现中,它的动作发布机制并不是完全隔离的。每次创建新的mockStore实例时,都会把自己的动作监听器绑定到全局的动作流上(或者说,每个mockStore都会订阅同一个动作源)。redux-observable的epic则是监听这个动作流,所以每多创建一个mockStore,epic就会多一个订阅通道,最终dispatch一次动作就会被多次接收。而store.getActions()只记录当前store实例的动作,所以看起来动作只派发了一次,但epic却收到了多次。
可行的解决方案
1. 复用单个mockStore实例,测试前重置状态
与其每次测试都创建新的mockStore,不如在测试套件开始时创建一次,然后在每个测试用例前重置它的动作记录和状态。这样就能避免多次订阅的问题:
// 全局声明store变量 let store; const initialState = { /* 你的初始状态 */ }; beforeEach(() => { // 如果还没创建过store,就创建一次;否则直接重置 if (!store) { store = mockStore(initialState); // 绑定epic到mockStore(如果需要的话) replaceEpic(yourEpic); } else { // 清空之前的动作记录 store.clearActions(); // 重置store的状态 store = mockStore(initialState); } }); afterEach(() => { // 可选:如果epic有手动订阅的逻辑,这里可以取消订阅 }); // 测试用例示例 test('dispatch FOO should trigger epic once', () => { store.dispatch({ type: 'FOO' }); // 断言动作只被记录一次 expect(store.getActions()).toHaveLength(1); // 断言epic只处理了一次动作(比如检查副作用触发次数) });
2. 每个测试后手动清理mockStore的订阅
如果你必须多次创建mockStore,可以尝试在每个测试用例结束后,清理mockStore内部的动作监听器。不过这个方法依赖redux-mock-store的内部实现,不同版本可能有差异:
afterEach(() => { // 假设mockStore实例有_listeners数组存储监听器,清空它 if (typeof store?._listeners !== 'undefined') { store._listeners = []; } // 如果有多个store实例,需要逐个清理 });
3. 改用真实Redux Store结合Mock Middleware
如果上面的方案都不满足需求,你可以考虑放弃redux-mock-store,改用真实的Redux Store,配合自定义的mock middleware来记录动作。这种方式隔离性更好,也能避免订阅重复的问题:
// 自定义mock middleware用于记录动作 const mockMiddleware = store => next => action => { store.getActions = () => store.actions; store.actions.push(action); return next(action); }; beforeEach(() => { // 每次测试创建新的真实store const store = createStore( rootReducer, initialState, applyMiddleware(mockMiddleware, createEpicMiddleware(yourEpic)) ); store.actions = []; });
总结
最稳妥的方案是第一种——复用单个mockStore实例并在测试前重置状态,这样既能保证测试隔离,又能避免多次订阅的问题。你提到的两个issue也说明了这是工具结合时的已知问题,目前的workaround就是通过手动隔离或清理来避免重复订阅。
内容的提问来源于stack exchange,提问作者Jalil

