如何作为一个单元正确测试 Redux Saga 中的 api 调用?

How to test api call in Redux Saga properly as a unit?

我想测试这个 saga API 函数:

export function* fetchEshopProjects(action: ProjectActionsGetEshopProjects) {
    yield put(projectActions.currentEshopProjectsLoading(true))

    const response: {
        projects: string[],
        error: Error
    } = yield call(ajax.json, '/new-project/eshops-projects/?eshopId=' + action.eshopId)

    if (response.error) {
        yield put(projectActions.currentEshopProjectsFailed(response.error))
        yield put(projectActions.currentEshopProjectsLoading(false))
    } else {
        yield put(projectActions.setCurrentEshopProjects(response.projects))
        yield put(projectActions.currentEshopProjectsLoading(false))
    }
}

我写了这个测试:

test('fetch projects', async() => {
        mockParams({
            locale: 'en-US',
        });

        ajax.json = jest.fn().mockResolvedValue(['kbp', 'tbp'])
        const dispatched = []
        await runSaga({
            dispatch: (action) => dispatched.push(action),
        },
            fetchEshopProjects
        )
        expect(dispatched).toEqual([
            {
                type: 'CURRENT_ESHOP_PROJECTS_LOADING',
                loadingState: true,
            },
            {
                type: 'SET_CURRENT_ESHOP_PROJECTS',
                payload: ['kbp', 'tbp'],
            },
            {
                type: 'CURRENT_ESHOP_PROJECTS_LOADING',
                loadingState: false,
            },
        ])
    })

似乎代码只到达 yield call(ajax.json... 部分然后 returns 返回到测试功能 - 因此只记录了第一次调度。我怎样才能更正测试以使其成功结束?

您忘记为 runSaga 返回的任务使用 task.toPromise。下面是一个工作示例:

saga.ts:

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

export const projectActions = {
  currentEshopProjectsLoading(loadingState) {
    return { type: 'CURRENT_ESHOP_PROJECTS_LOADING', loadingState };
  },
  setCurrentEshopProjects(payload) {
    return { type: 'SET_CURRENT_ESHOP_PROJECTS', payload };
  },
  currentEshopProjectsFailed(error) {
    return { type: 'CURRENT_ESHOP_PROJECTS_FAILED', error };
  },
};

export function* fetchEshopProjects(action) {
  yield put(projectActions.currentEshopProjectsLoading(true));

  const response: {
    projects: string[];
    error: Error;
  } = yield call(ajax.json, '/new-project/eshops-projects/?eshopId=' + action.eshopId);

  if (response.error) {
    yield put(projectActions.currentEshopProjectsFailed(response.error));
    yield put(projectActions.currentEshopProjectsLoading(false));
  } else {
    yield put(projectActions.setCurrentEshopProjects(response.projects));
    yield put(projectActions.currentEshopProjectsLoading(false));
  }
}

ajax.ts:

export const ajax = {
  async json(url) {},
};

saga.test.ts:

import { runSaga } from 'redux-saga';
import { ajax } from './ajax';
import { fetchEshopProjects } from './saga';

test('fetch projects', async () => {
  ajax.json = jest.fn().mockResolvedValue({ projects: ['kbp', 'tbp'] });
  const dispatched: any[] = [];
  await runSaga(
    {
      dispatch: (action) => dispatched.push(action),
    },
    fetchEshopProjects as any,
    { eshopId: '123' },
  ).toPromise();
  expect(dispatched).toEqual([
    {
      type: 'CURRENT_ESHOP_PROJECTS_LOADING',
      loadingState: true,
    },
    {
      type: 'SET_CURRENT_ESHOP_PROJECTS',
      payload: ['kbp', 'tbp'],
    },
    {
      type: 'CURRENT_ESHOP_PROJECTS_LOADING',
      loadingState: false,
    },
  ]);
});

测试结果:

 PASS   redux-saga-examples  packages/redux-saga-examples/src/Whosebug/71176872/saga.test.ts
  ✓ fetch projects (6 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |   81.25 |       75 |      60 |   81.25 |                   
 ajax.ts  |     100 |      100 |       0 |     100 |                   
 saga.ts  |      80 |       75 |      75 |      80 | 12,25-26          
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.623 s, estimated 3 s