所以我有这个使用
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 不执行任何异步操作。有什么建议吗?
如何对 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)
})
})
})
查看官方文档中的食谱:编写测试。另外,你在测试什么,动作创建者还是减速者?
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({ /* ... */ });
});
});
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])
});
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"}]})
})
})