Jest与Mobx:如何测试disposer中函数的执行?
问题需求
需要验证当appliedIds变化后,loadCampaign函数是否执行,同时要确认执行后的结果(比如campaign是否被正确赋值)。现有代码通过addDisposer设置了reaction监听,但Jest测试中无法正确等待reaction执行和异步操作完成。
相关代码
src/store/filters/campaignFilterStore.ts
import { reaction } from 'mobx'; import { Instance, addDisposer, flow, types } from 'mobx-state-tree'; export const CampaignFull = types.model('CampaignFull', { id: types.identifierNumber, name: types.string, userId: types.number, }); export type ICampaignFull = Instance<typeof CampaignFull>; interface ICampaignData { id: number; name: string; userId: number; } export const CampaignFilterStore = types .model('CampaignFilterStore', { appliedIds: types.array(types.number), campaignIds: types.array(types.number), campaign: types.maybe(CampaignFull), }) .actions((self) => ({ afterCreate(): void { console.log('afterCreate'); addDisposer( self, reaction(() => self.appliedIds, this.loadCampaign), ); }, loadCampaign: flow(function* loadCampaign(): Generator< Promise<ICampaignData>, void, ICampaignData > { const campaignId = self.appliedIds[0]; console.log('loadCampaign', campaignId); if (!campaignId) { return; } try { const data = yield new Promise((res) => { setTimeout(() => { res({ id: 1, name: 'name', userId: 12, }); }, 500); }); self.campaign = CampaignFull.create({ id: data.id, name: data.name, userId: data.userId, }); } catch (e) { console.error(e); } }), apply(): void { self.appliedIds.replace([...self.campaignIds]); }, toggleCampaign(campaign: { id: number; name: string }): void { self.campaignIds.push(campaign.id); }, }));
src/store/filters/campaignFilterStore.test.ts
import { isObservable } from 'mobx'; import { CampaignFilterStore } from 'store/filters/campaignFilterStore'; const CAMPAIGN_1 = { id: 1, name: 'name', userId: 12, }; describe('CampaignFilterStore', () => { test('should load campaign by disposer - DOESNT WORK', async () => { const campaigns = CampaignFilterStore.create(); campaigns.toggleCampaign(CAMPAIGN_1); expect(campaigns.campaign).toBeUndefined(); try { expect(isObservable(campaigns.appliedIds)).toBe(true); campaigns.apply(); expect(campaigns.appliedIds).toEqual([CAMPAIGN_1.id]); } finally { // campaign SHOULD be loaded expect(campaigns.campaign).not.toBeUndefined(); } }); test('should load campaign', async () => { const campaigns = CampaignFilterStore.create({ appliedIds: [CAMPAIGN_1.id], }); expect(campaigns.campaign).toBeUndefined(); await campaigns.loadCampaign(); expect(campaigns.campaign).not.toBeUndefined(); expect(campaigns.campaign?.name).toEqual(CAMPAIGN_1.name); }); });
解决方法
1. 修复Store中的Reaction配置
原代码的reaction存在两个问题:this指向错误、数组引用未变化导致不触发回调。修改afterCreate方法:
afterCreate(): void { console.log('afterCreate'); addDisposer( self, // 返回数组副本触发浅比较,同时直接调用self.loadCampaign确保上下文正确 reaction(() => [...self.appliedIds], () => self.loadCampaign()), ); }
如果不想返回副本,也可以开启深度比较:
reaction(() => self.appliedIds, () => self.loadCampaign(), { deep: true })
2. 编写正确的Jest测试
有两种方式等待异步操作完成:
方式一:使用waitFor(推荐)
借助@testing-library/react的waitFor等待异步结果:
import { isObservable } from 'mobx'; import { waitFor } from '@testing-library/react'; import { CampaignFilterStore } from 'store/filters/campaignFilterStore'; const CAMPAIGN_1 = { id: 1, name: 'name', userId: 12, }; describe('CampaignFilterStore', () => { test('should load campaign when appliedIds changes', async () => { const store = CampaignFilterStore.create(); store.toggleCampaign(CAMPAIGN_1); expect(store.campaign).toBeUndefined(); store.apply(); expect(store.appliedIds).toEqual([CAMPAIGN_1.id]); // 等待异步操作完成,断言campaign赋值结果 await waitFor(() => { expect(store.campaign).not.toBeUndefined(); expect(store.campaign?.name).toBe(CAMPAIGN_1.name); }); }); test('should load campaign when called directly', async () => { const store = CampaignFilterStore.create({ appliedIds: [CAMPAIGN_1.id], }); expect(store.campaign).toBeUndefined(); await store.loadCampaign(); expect(store.campaign).not.toBeUndefined(); expect(store.campaign?.name).toEqual(CAMPAIGN_1.name); }); });
方式二:使用Jest Fake Timers
如果不想额外依赖库,用Jest的fake timers控制时序:
test('should load campaign when appliedIds changes (fake timers)', async () => { jest.useFakeTimers(); const store = CampaignFilterStore.create(); store.toggleCampaign(CAMPAIGN_1); store.apply(); // 触发reaction同步逻辑 jest.runAllTicks(); // 快进所有定时器 jest.runAllTimers(); // 等待异步Promise完成 await Promise.resolve(); expect(store.campaign).not.toBeUndefined(); expect(store.campaign?.name).toBe(CAMPAIGN_1.name); jest.useRealTimers(); });
3. 验证loadCampaign被调用(可选)
如果需要明确验证函数是否触发,可以spyOn该方法:
test('should trigger loadCampaign when appliedIds changes', async () => { const store = CampaignFilterStore.create(); const loadSpy = jest.spyOn(store, 'loadCampaign'); store.toggleCampaign(CAMPAIGN_1); store.apply(); // 等待reaction执行 jest.runAllTicks(); expect(loadSpy).toHaveBeenCalled(); // 等待异步完成 jest.runAllTimers(); await Promise.resolve(); expect(store.campaign).not.toBeUndefined(); });
内容的提问来源于stack exchange,提问作者Ekaterina Kornilova
相关产品推荐
相关产品推荐

