测试 createAsyncThunk Redux Toolkit Jest

Posted

技术标签:

【中文标题】测试 createAsyncThunk Redux Toolkit Jest【英文标题】:Testing createAsyncThunk Redux Toolkit Jest 【发布时间】:2020-09-26 21:15:58 【问题描述】:

我是 redux 工具包库的新手,尤其是在测试方面。我浏览了文档并阅读了一堆关于这个主题的帖子和文章,但仍然很挣扎。我构建了一个简单的待办事项应用程序,并包含了几个 API 请求来涵盖异步情况。不过,测试这些结果有点挑战性。我希望就我的代码以及可以改进的地方获得一些建议和反馈。我还想对测试 createAsyncThunk 切片是否有意义提出一些意见。注意:我对测试 API 调用本身不感兴趣,并使用模拟数据重新创建成功的请求。

建设性的批评非常有帮助,我们将不胜感激

请看一下我的切片文件并进行测试

postsSlice.ts

import  createSlice, createAsyncThunk  from "@reduxjs/toolkit";
import  RootState  from "../../store";
import axios from "axios";

export type Post = 
  userId: number;
  id: number;
  title: string;
  body: string;
;

export type PostsState = 
  posts: Post[];
  loading: boolean;
  error: null | string;
;

export const initalPostState: PostsState = 
  posts: [],
  loading: false,
  error: null,
;

export const fetchAllPosts = createAsyncThunk(
  "posts/allPosts",
  async (data,  rejectWithValue ) => 
    try 
      const response = await axios.get(
        `https://jsonplaceholder.typicode.com/posts`
      );
      return (await response.data) as Post[];
     catch (err) 
      if (!err.response) 
        throw err;
      
      return rejectWithValue(err.response.data);
    
  
);

export const fetchSuccessful = fetchAllPosts.fulfilled;
export const fetchPending = fetchAllPosts.pending;
export const fetchFailed = fetchAllPosts.rejected;

const postsSlice = createSlice(
  name: "Posts",
  initialState: initalPostState,
  reducers: ,
  extraReducers: (builder) => 
    builder.addCase(fetchSuccessful, (state,  payload ) => 
      state.posts = payload;
      state.loading = false;
    );
    builder.addCase(fetchPending, (state, action) => 
      state.loading = true;
    );
    builder.addCase(fetchFailed, (state, action) => 
      state.error = action.error.message
        ? action.error.message
        : "Failed to load data";
      state.loading = false;
    );
  ,
);

export const selectPosts = (state: RootState) => state.fetchedPosts;
export const fetchedPostsReducer = postsSlice.reducer;

测试

postsSlice.test.ts

import 
  initalPostState,
  fetchPending,
  fetchFailed,
  selectPosts,
  fetchSuccessful,
  fetchedPostsReducer,
 from "./postsSlice";
import  Post, PostsState  from "./postsSlice";
import store,  RootState  from "../../store";

const appState = store.getState();

describe("postsSlice", () => 
  describe("Posts State, Posts Action and Selector", () => 
    it("should set loading state on true when API call is pending", async (done) => 
      // Arrange

      // Act
      const nextState: PostsState = await fetchedPostsReducer(
        initalPostState,
        fetchPending
      );
      // Assert
      const rootState: RootState =  ...appState, fetchedPosts: nextState ;
      expect(selectPosts(rootState).loading).toBeTruthy();
      expect(selectPosts(rootState).error).toBeNull();
      done();
    );

    it("should set error state when API call is rejected", async (done) => 
      // Arrange
      const response = 
        message: "Network request failed",
        name: "error",
      ;
      // Act
      const nextState: PostsState = await fetchedPostsReducer(
        initalPostState,
        fetchFailed(response, "")
      );
      // Assert
      const rootState: RootState =  ...appState, fetchedPosts: nextState ;
      expect(selectPosts(rootState).loading).toBeFalsy();
      expect(selectPosts(rootState).error).not.toBeNull();
      expect(selectPosts(rootState).error).toEqual("Network request failed");
      done();
    );

    it("should update state when API call is successful", async (done) => 
      // Arrange
      const response: Post[] = [
        
          userId: 1,
          id: 1,
          title:
            "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
          body:
            "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
        ,
        
          userId: 1,
          id: 2,
          title: "qui est esse",
          body:
            "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla",
        ,
      ];
      // Act
      const nextState: PostsState = await fetchedPostsReducer(
        initalPostState,
        fetchSuccessful(response, "")
      );
      // Assert
      const rootState: RootState =  ...appState, fetchedPosts: nextState ;
      expect(selectPosts(rootState).loading).toBeFalsy();
      expect(selectPosts(rootState).error).toBeNull();
      expect(selectPosts(rootState).posts).toEqual(
        expect.arrayContaining(response)
      );
      done();
    );
  );
);

【问题讨论】:

如果您有工作代码,那么问题与 SO 无关,请使用 codereview.stackexchange.com 。 async (done) => 不好。使用 Promise 时不需要 done,这是特定于 Jest 而不是 Redux。 fetchAllPosts 可以用github.com/axios/moxios 进行测试。 【参考方案1】:

我已经在 GitHub 上回答了 redux 工具包,但我也会在这里发布,因为这是我在尝试自己的解决方案之前访问的众多链接之一。

说明

由于createAsyncThunk 返回一个函数供以后执行,您可以利用它来发挥自己的优势。无需费心测试整个商店与您的 thunk 的交互,您可以在远离商店的情况下单独测试 thunk。

运行您的 jest.mock 调用来模拟您可能用于访问服务器或本地状态的任何 API/挂钩,更改这些解析/返回的内容,然后执行您保存的方法。这样做可以让您使用您通常调用的参数访问 createAsyncThunk 调用中的 promise / 方法。

具体问题

您不想测试 store,而是要测试 thunk 是否正在调度 store 所依赖的操作来设置诸如加载、要保存的错误消息等。这样您可以改为为您的减速器创建测试,您可以在每次测试时重新创建一个全新的商店,并确保您通过这些减速器进行的所有转换都是正确的。


重击

// features/account/thunks.ts

import api from './api';                    // http calls to the API
import  actions  from './reducer';        // "actions" from a createSlice result
import  useRefreshToken  from './hooks';  // a `useSelector(a => a.account).auth?.refreshToken` result

// declare and code as normal
export const register = createAsyncThunk(
  'accounts/register',
  async (arg: IRegisterProps,  dispatch ) => 
    try 
      const data = await api.register(arg);
      dispatch(actions.authSuccess(data));
     catch (err) 
      console.error('Unable to register', err);
    
  
);

// Using a hook to access state
export const refreshSession = createAsyncThunk(
  'accounts/refreshSession',
  async (_,  dispatch ) => 
    // or add `, getState` beside dispatch and do token = getState().accounts.auth.refreshToken;
    // If you use getState, your test will be more verbose though
    const token: string = useRefreshToken();
    try 
      const data = await api.refreshToken(token);
      dispatch(actions.tokenRefreshed(data));
     catch (err) 
      console.error('Unable to refresh token', err);
    
  
);


测试

// features/account/thunks.test.ts

import apiModule from './api';
import hookModule from './hooks';
import thunks from './thunks';

import  actions  from './reducer';
import  IRegisterProps  from './types';
import  AsyncThunkAction, Dispatch  from '@reduxjs/toolkit';
import  IAuthSuccess  from 'types/auth';

jest.mock('./api');
jest.mock('./hooks')

describe('Account Thunks', () => 
  let api: jest.Mocked<typeof apiModule>;
  let hooks: jest.Mocked<typeof hookModule>

  beforeAll(() => 
    api = apiModule as any;
    hooks = hookModule as any;
  );

  // Clean up after yourself.
  // Do you want bugs? Because that's how you get bugs.
  afterAll(() => 
    jest.unmock('./api');
    jest.unmock('./hooks');
  );

  describe('register', () => 

    // We're going to be using the same argument, so we're defining it here
    // The 3 types are <What's Returned, Argument, Thunk Config>
    let action: AsyncThunkAction<void, IRegisterProps, >;
    
    let dispatch: Dispatch;        // Create the "spy" properties
    let getState: () => unknown;

    let arg: IRegisterProps;
    let result: IAuthSuccess;

    beforeEach(() => 
      // initialize new spies
      dispatch = jest.fn();
      getState = jest.fn();

      api.register.mockClear();
      api.register.mockResolvedValue(result);

      arg =  email: 'me@myemail.com', password: 'yeetmageet123' ;
      result =  accessToken: 'access token', refreshToken: 'refresh token' ;

      action = thunks.registerNewAccount(arg);
    );

    // Test that our thunk is calling the API using the arguments we expect
    it('calls the api correctly', async () => 
      await action(dispatch, getState, undefined);
      expect(api.register).toHaveBeenCalledWith(arg);
    );

    // Confirm that a success dispatches an action that we anticipate
    it('triggers auth success', async () => 
      const call = actions.authSuccess(result);
      await action(dispatch, getState, undefined);
      expect(dispatch).toHaveBeenCalledWith(call);
    );
  );

  describe('refreshSession', () => 
    // We're going to be using the same argument, so we're defining it here
    // The 3 types are <What's Returned, Argument, Thunk Config>
    let action: AsyncThunkAction<void, unknown, >;
    
    let dispatch: Dispatch;        // Create the "spy" properties
    let getState: () => unknown;

    let result: IAuthSuccess;
    let existingToken: string;

    beforeEach(() => 
      // initialize new spies
      dispatch = jest.fn();
      getState = jest.fn();

      existingToken = 'access-token-1';

      hooks.useRefreshToken.mockReturnValue(existingToken);

      api.refreshToken.mockClear();
      api.refreshToken.mockResolvedValue(result);

      result =  accessToken: 'access token', refreshToken: 'refresh token 2' ;

      action = thunks.refreshSession();
    );

    it('does not call the api if the access token is falsy', async () => 
      hooks.useRefreshToken.mockReturnValue(undefined);
      await action(dispatch, getState, undefined);
      expect(api.refreshToken).not.toHaveBeenCalled();
    );

    it('uses a hook to access the token', async () => 
      await action(dispatch, getState, undefined);
      expect(hooks.useRefreshToken).toHaveBeenCalled();
    );

    // Test that our thunk is calling the API using the arguments we expect
    it('calls the api correctly', async () => 
      await action(dispatch, getState, undefined);
      expect(api.refreshToken).toHaveBeenCalledWith(existingToken);
    );

    // Confirm a successful action that we anticipate has been dispatched too
    it('triggers auth success', async () => 
      const call = actions.tokenRefreshed(result);
      await action(dispatch, getState, undefined);
      expect(dispatch).toHaveBeenCalledWith(call);
    );
  );
);


【讨论】:

我到处都看到了这个答案,这是错误的(或者可能缺少一些代码)。例如:thunks. registerNewAccount 这是哪里来的?而thunks.ts 本身没有export default,从技术上讲import thunks from './thunks'; 不起作用 import thunks from './thunks'; 表示您有一个名为thunks.ts 的文件,其中填充了const example = createAsyncThunk…。该代码没有丢失,因为该代码应该是您的;)

以上是关于测试 createAsyncThunk Redux Toolkit Jest的主要内容,如果未能解决你的问题,请参考以下文章

如何从 Redux Toolkit 中去抖 createAsyncThunk

createAsyncThunk 和使用 redux-toolkit 编写 reducer 登录

如何从 React 组件 Redux Toolkit 中的 createAsyncThunk 获取结果

如何在 redux 工具包中将参数传递给 createAsyncThunk?

Redux-Toolkit createAsyncThunk Dispatch 显示为未定义

我可以使用 redux 工具包访问带有 axios 的 createAsyncThunk 中的状态吗?