首页 > 解决方案 > How to test api calls within redux-saga

问题描述

I have this saga effects that calls to an API and dispatched an action if it is successful:

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

This is the helper function:

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

The test suit for this saga looks like this:

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

However the test that expects the getStuffMock function to have been called fails. How can I fix this? I am using jest with testing-library

标签: reactjstestingreact-reduxjestjsredux-saga

解决方案


call(fn, ...args) is just a function that returns a plain Effect Object. It will not execute the fn call immediately. When testing the saga generator function step-by-step, you manually execute the generator and provide the value of yield by .next () method, the getStuff function will not execute.

The call(getStuff) just returns a Effect object like this:

{
  CALL: {
    fn: getStuff,
  }
}

If you want to execute the mocked getStuff function, you need to test the saga in this way - Testing the full Saga.

runSaga will get the Effect object and execute the function it holds.

Test example:

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

test result:

 PASS   redux-saga-examples  packages/redux-saga-examples/src/stackoverflow/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

推荐阅读