如何在 redux-saga 中测试 api 调用

How to test api calls within redux-saga

我有这个调用 API 并在成功时发送操作的 saga 效果:

export function* getThemEffect() {
  try {
    yield put(requestActoin());
    const data: AxiosResponse<ServerResponseSchema> = yield call(getStuff);
    yield put(successAction(data.data.data));
  } catch (err: any) {
    yield put(failureAction(err?.response?.data || null));
  }
}

这是辅助函数:

export function getStuff() {
  const config: AxiosRequestConfig = {
    method: "GET",
    url: "https://somewhere.com/api/get"
  };
  return axios(config);
}

这个 saga 的测试服看起来像这样:

import * as api from "../api";

const getStuffSpy = jest.spyOn(api, "getStuff");

describe("search saga", () => {
   let gen: Generator, response: any, getStuffMock: jest.Mock;
   beforeEach(() => {
      getStuffSpy.mockClear();
      gen = getThemEffect();
      getStuffMock = jest.fn();
      getStuffSpy.mockImplementation(getStuffMock);
    });
   describe("server success response", () => {
      beforeEach(() => {
        response = { data: { data: ["1", "2", "3"] } };
      });
      it("should create correct success flow", () => {
        expect(gen.next()).toEqual({
          value: put(requestAction()),
          done: false
        });
        expect(gen.next()).toEqual({
          value: call(api.getStuff),
          done: false
        });
        expect(getStuffMock).toHaveBeenCalled(); // <=== this fails
        expect(gen.next(response)).toEqual({
          value: put(successAction(["1", "2", "3"])),
          done: false
        });
        expect(gen.next()).toEqual({
          value: undefined,
          done: true
        });
      });
    });
}

然而,期望 getStuffMock 函数被调用的测试失败了。我怎样才能解决这个问题?我正在使用 testing-library

开玩笑

call(fn, ...args) 只是一个 returns 普通 Effect 对象的函数。它不会立即执行 fn 调用。一步步测试saga生成器函数时,你手动执行生成器,通过.next ()方法提供yield的值,getStuff函数不会执行。

call(getStuff)只是returns一个效果对象,像这样:

{
  CALL: {
    fn: getStuff,
  }
}

如果你想执行模拟的getStuff函数,你需要用这种方式测试saga - Testing the full Saga.

runSaga 将获取 Effect 对象并执行它持有的函数。

测试示例:

saga.ts:

import { call, put } from 'redux-saga/effects';
import { getStuff } from './api';

export const requestAction = () => ({ type: 'REQUEST' });
export const successAction = (data) => ({ type: 'SUCCESS', payload: data });
export const failureAction = (error) => ({ type: 'FAILURE', payload: error, error: true });

export function* getThemEffect() {
  try {
    yield put(requestAction());
    const data = yield call(getStuff);
    yield put(successAction(data.data.data));
  } catch (err: any) {
    yield put(failureAction(err?.response?.data || null));
  }
}

api.ts:

import axios, { AxiosRequestConfig } from 'axios';

export function getStuff() {
  const config: AxiosRequestConfig = {
    method: 'GET',
    url: 'https://somewhere.com/api/get',
  };
  return axios(config);
}

saga.test.ts:

import { runSaga } from '@redux-saga/core';
import { call, put } from '@redux-saga/core/effects';
import { mocked } from 'ts-jest/utils';
import { getStuff } from './api';
import { getThemEffect, requestAction, successAction } from './saga';

jest.mock('./api');

const getStuffMock = mocked(getStuff);

describe('search saga', () => {
  it('should create correct success flow', () => {
    const gen = getThemEffect();
    const response = { data: { data: ['1', '2', '3'] } };
    expect(gen.next()).toEqual({
      value: put(requestAction()),
      done: false,
    });
    expect(gen.next()).toEqual({
      value: call(getStuff),
      done: false,
    });

    expect(gen.next(response)).toEqual({
      value: put(successAction(['1', '2', '3'])),
      done: false,
    });
    expect(gen.next()).toEqual({
      value: undefined,
      done: true,
    });
  });

  it('should pass', async () => {
    const response = { data: { data: ['1', '2', '3'] } };
    const dispatched: any[] = [];
    getStuffMock.mockResolvedValueOnce(response as any);
    await runSaga(
      {
        dispatch: (action) => dispatched.push(action),
        getState: () => ({}),
      },
      getThemEffect,
    ).toPromise();
    expect(dispatched).toEqual([{ type: 'REQUEST' }, { type: 'SUCCESS', payload: ['1', '2', '3'] }]);
    expect(getStuffMock).toHaveBeenCalled();
  });
});

测试结果:

 PASS   redux-saga-examples  packages/redux-saga-examples/src/Whosebug/69371886/saga.test.ts
  search saga
    ✓ should create correct success flow (4 ms)
    ✓ should pass (3 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |   80.95 |        0 |      60 |   78.57 |                   
 api.ts   |      50 |      100 |       0 |      50 | 4-8               
 saga.ts  |   88.24 |        0 |      75 |      90 | 14                
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.662 s