【问题标题】:How to test api calls within redux-saga如何在 redux-saga 中测试 api 调用
【发布时间】:2021-11-21 02:17:06
【问题描述】:

我有这样的传奇效果,它调用 API 并在成功时分派一个动作:

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);
}

这个传奇的测试服如下所示:

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 函数已被调用的测试失败。我怎样才能解决这个问题?我在测试库中使用 jest

【问题讨论】:

    标签: reactjs testing react-redux jestjs redux-saga


    【解决方案1】:

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

    call(getStuff) 只返回一个 Effect 对象,如下所示:

    {
      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/*/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
    

    【讨论】:

      最近更新 更多