如何对这个 Redux thunk 进行单元测试?

问题描述 投票:0回答:4

所以我有这个使用

redux thunk
中间件的 Redux 操作创建器:

accountDetailsActions.js:

export function updateProduct(product) {
  return (dispatch, getState) => {
    const { accountDetails } = getState();

    dispatch({
      type: types.UPDATE_PRODUCT,
      stateOfResidence: accountDetails.stateOfResidence,
      product,
    });
  };
}

如何测试?我正在使用

chai
包进行测试。我在网上找到了一些资源,但不确定如何继续。这是我到目前为止的测试:

accountDetailsReducer.test.js:

describe('types.UPDATE_PRODUCT', () => {
    it('should update product when passed a product object', () => {
        //arrange
        const initialState = {
            product: {}
        };
        const product = {
            id: 1,
            accountTypeId: 1,
            officeRangeId: 1,
            additionalInfo: "",
            enabled: true
        };
        const action = actions.updateProduct(product);
        const store = mockStore({courses: []}, action);
        store.dispatch(action);
        //this is as far as I've gotten - how can I populate my newState variable in order to test the `product` field after running the thunk?
        //act
        const newState = accountDetailsReducer(initialState, action);
        //assert
        expect(newState.product).to.be.an('object');
        expect(newState.product).to.equal(product);
    });
});

我的 thunk 不执行任何异步操作。有什么建议吗?

javascript redux chai
4个回答
20
投票

如何对 Redux Thunk 进行单元测试

thunk 动作创建者的全部目的是在将来分派异步动作。使用 redux-thunk 时,一个好的方法是对开始和结束的异步流程进行建模,通过三个操作导致成功或错误。

虽然此示例使用 Mocha 和 Chai 进行测试,但您可以轻松地使用任何断言库或测试框架。

使用由我们的主要 thunk 动作创建者管理的多个动作对异步流程进行建模

在此示例中,我们假设您想要执行更新产品的异步操作,并且想要了解三个关键事项。

  • 当异步操作开始时
  • 当异步操作完成时
  • 异步操作是否成功

好的,现在是时候根据操作生命周期的这些阶段来建模我们的 redux 操作了。请记住,这同样适用于所有异步操作,因此这通常适用于从 api 获取数据的 http 请求。

我们可以这样写我们的动作。

accountDetailsActions.js:

export function updateProductStarted (product) {
  return {
    type: 'UPDATE_PRODUCT_STARTED',
    product,
    stateOfResidence
  }
}

export function updateProductSuccessful (product, stateOfResidence, timeTaken) {
  return {
    type: 'PRODUCT_UPDATE_SUCCESSFUL',
    product,
    stateOfResidence
    timeTaken
  }
}

export function updateProductFailure (product, err) {
  return {
    product,
    stateOfResidence,
    err
  }
}

// our thunk action creator which dispatches the actions above asynchronously
export function updateProduct(product) {
  return dispatch => {
    const { accountDetails } = getState()
    const stateOfResidence = accountDetails.stateOfResidence

    // dispatch action as the async process has begun
    dispatch(updateProductStarted(product, stateOfResidence))

    return updateUser()
        .then(timeTaken => {
           dispatch(updateProductSuccessful(product, stateOfResidence, timeTaken)) 
        // Yay! dispatch action because it worked
      }
    })
    .catch(error => {
       // if our updateUser function ever rejected - currently never does -
       // oh no! dispatch action because of error
       dispatch(updateProductFailure(product, error))

    })
  }
}

注意底部忙碌的动作。这就是我们的 thunk 动作创建者。由于它返回一个函数,因此它是一个被 redux-thunk 中间件拦截的特殊操作。那个 thunk 动作创建者可以在未来的某个时刻派遣其他动作创建者。很聪明。

现在我们已经编写了对异步过程(即用户更新)进行建模的操作。假设这个过程是一个返回承诺的函数调用,这将是当今处理异步过程的最常见方法。

为我们使用 redux 操作建模的实际异步操作定义逻辑

对于这个例子,我们将只创建一个返回 Promise 的通用函数。将其替换为更新用户或执行异步逻辑的实际函数。确保该函数返回一个承诺。

我们将使用下面定义的函数来创建一个工作的独立示例。要获得一个工作示例,只需将此函数放入您的操作文件中,这样它就在您的 thunk 操作创建者的范围内。

 // This is only an example to create asynchronism and record time taken
 function updateUser(){
      return new Promise( // Returns a promise will be fulfilled after a random interval
          function(resolve, reject) {
              window.setTimeout(
                  function() {
                      // We fulfill the promise with the time taken to fulfill
                      resolve(thisPromiseCount);
                  }, Math.random() * 2000 + 1000);
          }
      )
})

我们的测试文件

import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import chai from 'chai' // You can use any testing library
let expect = chai.expect;

import { updateProduct } from './accountDetailsActions.js'

const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares)

describe('Test thunk action creator', () => {
  it('expected actions should be dispatched on successful request', () => {
    const store = mockStore({})
    const expectedActions = [ 
        'updateProductStarted', 
        'updateProductSuccessful'
    ]

    return store.dispatch(fetchSomething())
      .then(() => {
        const actualActions = store.getActions().map(action => action.type)
        expect(actualActions).to.eql(expectedActions)
     })

  })

  it('expected actions should be dispatched on failed request', () => {
    const store = mockStore({})
    const expectedActions = [ 
        'updateProductStarted', 
        'updateProductFailure'
    ]

    return store.dispatch(fetchSomething())
      .then(() => {
        const actualActions = store.getActions().map(action => action.type)
        expect(actualActions).to.eql(expectedActions)
     })

  })
})

7
投票

查看官方文档中的食谱:编写测试。另外,你在测试什么,动作创建者还是减速者?

Action Creator 测试示例

describe('types.UPDATE_PRODUCT', () => {
    it('should update product when passed a product object', () => {    
        const store = mockStore({courses: []});
        const expectedActions = [
            / * your expected actions */
        ];

        return store.dispatch(actions.updateProduct(product))
            .then(() => {
                expect(store.getActions()).to.eql(expectedActions);
            });
    });
});

减速机测试示例

您的减速器应该是纯函数,因此您可以在商店环境之外单独测试它。

const yourReducer = require('../reducers/your-reducer');

describe('reducer test', () => {
    it('should do things', () => {
        const initialState = {
            product: {}
        };

        const action = {
            type: types.UPDATE_PRODUCT,
            stateOfResidence: // whatever values you want to test with,
            product: {
                id: 1,
                accountTypeId: 1,
                officeRangeId: 1,
                additionalInfo: "",
                enabled: true
            }
        }

        const nextState = yourReducer(initialState, action);

        expect(nextState).to.be.eql({ /* ... */ });
    });
});

0
投票
export const someAsyncAction = (param) => (dispatch, getState) => {
    const { mock } = getState();
    dispatch({
        type: 'SOME_TYPE',
        mock: mock + param,
    })
}

it('should test someAsyncAction', () => {
    const param = ' something';
    const dispatch = jest.fn().mockImplementation();
    const getState = () => ({
        mock: 'mock value',
    });

    const expectedAction = {
        type: 'SOME_TYPE',
        mock: 'mock value something'
    };

    const callback = someAsyncAction(param);
    expect(typeof callback).toBe('function');

    callback.call(this, dispatch, getState);
    expect(dispatch.mock.calls[0]).toEqual([expectedAction])
});

0
投票
import {createStore, applyMiddleWare, combineReducers} from 'redux';
import contactsReducer from '../reducers/contactsReducer';

function getThunk(dispatchMock){

 return ({dispatch, getState}) => (next) => (action) => {

  if(typeof action === 'function'){
   // dispatch mock function whenever we are dispatching thunk action
   action(dispatchMock, getState);

   return action(dispatch, getState);
  }
  // dispatch mock function whenever we are dispatching action
  dispatchMock(action);
  return next(action);
 }

}

describe('Test Redux reducer with Thunk Actions', ()=>{

 test('should add contact on dispatch of addContact action',()=>{
   // to track all actions dispatched from store, creating mock.
   const storeDispatchMock = jest.fn();

   const reducer = combineReducers({
     Contacts: contactsReducer
   })

   const store = createStore( reducer, {Contacts:[]}, applyMiddleware(getThunk(storeDispatchMock));

   store.dispatch(someThunkAction({name:"test1"}))
   expect(storeDispatchMock).toHaveBeenCalledWith({
    type:'contactUpdate', payload:{{name:"test1"}}
   })
   expect(store.getState()).toBe({Contacts:[{name:"test1"}]})
 })

})
© www.soinside.com 2019 - 2024. All rights reserved.