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

Redux Saga生成器函数Jest测试:重复yield fetch问题排查

Redux Saga生成器函数测试问题解答

问题场景

目标Saga代码

export function* mainFunction({ payload: { authorization } }) {
  try {
    const response = yield call(
      fetch,
      `${url}/api`,
      {
        headers: {
          Authorization: authorization ,
        },
      }
    );

    if (response.status !== 200) {
      throw new Error("Failed to load");
    }

    const jsonResponse = yield response.json();

    const data = yield call(
      helperFunction1,
      Array.from(jsonResponse)
    );

    const updatedData = yield call(helperFunction2, data);

    yield put(setData({ finalData: updatedData }));
    yield put(helperFunction3({ authorization }));
  } catch ({ message }) {
    yield showError(message);
    yield put(setError(message));
  }
}

对应Jest测试代码

const payload = {
      authorization: "authorization",
    };
const apiCall = call(
      fetch,
      `${url}/api`,
      {
        headers: {
          Authorization: authorization,
        },
      }
    );
 describe("success", () => {
      const saga = mainFunction({ payload });
      const data = "data";
      const response = {
        status: 200,
        json: () => data,
      };
      const jsonResponse = response.json();

      it("should call API", () => {
        expect(saga.next().value).toEqual(apiCall);
      });

      it("should get response with data", () => {
        expect(saga.next(response).value).toEqual(data);
      });

      it("should call helperFunction1", () => {
        expect(saga.next(jsonResponse).value).toEqual(
          call(helperFunction1, Array.from(jsonResponse))
        );
      });

      it("should call helperFunction2", () => {
        expect(saga.next(data).value).toEqual(call(helperFunction2, data));
      });

      it("should dispatch success action with data", () => {
        expect(saga.next(data).value).toEqual(
          put(setData({ finalData: data }))
        );
      });

      it("should dispatch status check", () => {
        expect(saga.next(data).value).toEqual(
          put(helperFunction3({ accessToken: "token" }))
        );
      });

      it("should be done", () => {
        expect(saga.next().done).toBe(true);
      });
    });

问题现象

前两个测试用例运行正常,但第三个验证helperFunction1调用的用例中,实际yield的是fetch调用(第一个测试的预期结果),而非预期的helperFunction1调用;后续测试却能正常通过。所有辅助函数均非生成器函数。


问题原因

核心问题是生成器实例的状态在测试用例间没有隔离:

  • 你在describe块顶部只初始化了一次saga生成器实例,但Jest的每个it测试用例是独立执行的,用例间不会保留生成器的推进状态。
  • 第一个it调用saga.next(),消耗了生成器的第一个yield(fetch调用);但第二个it执行时,拿到的是初始状态的生成器,直接调用saga.next(response)相当于跳过了fetch的yield步骤,导致生成器内部逻辑混乱。
  • 到第三个it时,生成器又回到初始状态,调用saga.next(jsonResponse)自然会得到第一个yield的fetch调用,这就是你看到重复yield fetch的原因。
  • 后续测试看似正常,只是因为每次操作的都是新的初始生成器,碰巧参数和步骤对上了,但整个测试逻辑完全错误。

解决方法

在每个测试用例内部或beforeEach钩子中重新初始化生成器,确保每个测试都从生成器的初始状态开始,按正确顺序推进:

describe("success", () => {
  let saga;
  const payload = { authorization: "authorization" };
  const data = "data";
  const response = { status: 200, json: () => data };
  const jsonResponse = response.json();
  const apiCall = call(fetch, `${url}/api`, {
    headers: { Authorization: payload.authorization },
  });

  // 每个测试前重新创建生成器实例
  beforeEach(() => {
    saga = mainFunction({ payload });
  });

  it("should call API", () => {
    expect(saga.next().value).toEqual(apiCall);
  });

  it("should get response with data", () => {
    saga.next(); // 先推进到fetch的yield
    expect(saga.next(response).value).toEqual(jsonResponse);
  });

  it("should call helperFunction1", () => {
    saga.next(); // 推进fetch步骤
    saga.next(response); // 推进response.json()步骤
    expect(saga.next(jsonResponse).value).toEqual(
      call(helperFunction1, Array.from(jsonResponse))
    );
  });

  it("should call helperFunction2", () => {
    saga.next();
    saga.next(response);
    saga.next(jsonResponse);
    expect(saga.next(data).value).toEqual(call(helperFunction2, data));
  });

  it("should dispatch success action with data", () => {
    saga.next();
    saga.next(response);
    saga.next(jsonResponse);
    saga.next(data);
    expect(saga.next(data).value).toEqual(
      put(setData({ finalData: data }))
    );
  });

  it("should dispatch status check", () => {
    saga.next();
    saga.next(response);
    saga.next(jsonResponse);
    saga.next(data);
    saga.next(data);
    expect(saga.next().value).toEqual(
      put(helperFunction3({ authorization: payload.authorization }))
    );
  });

  it("should be done", () => {
    saga.next();
    saga.next(response);
    saga.next(jsonResponse);
    saga.next(data);
    saga.next(data);
    saga.next();
    expect(saga.next().done).toBe(true);
  });
});

另外注意修正原测试中的一个小错误:最后一个put(helperFunction3)的参数应该是{ authorization }而非{ accessToken: "token" },和目标Saga代码保持一致。

内容的提问来源于stack exchange,提问作者AppSupportGuru1995

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 20:00:16