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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 13:45:13