使用 Typescript 测试 redux 异步操作
Testing redux async actions with Typescript
大家好,我正在尝试使用 Typescript 测试我的异步操作创建器,但我遇到了一个我无法解决的类型错误。
这是我的操作:
export const loadCurrentUserRequest = () => ({
type: LOAD_CURRENT_USER_REQUEST
})
export const loadCurrentUserSuccess = (payload: any) => ({
type: LOAD_CURRENT_USER_SUCCESS,
payload
})
export const loadCurrentUserFailure = (payload: any) => ({
type: LOAD_CURRENT_USER_FAILURE,
payload
})
这是我的异步动作创建者:
export const loadCurrentUser = () => {
return async (dispatch: Dispatch<any>) => {
dispatch(loadCurrentUserRequest())
try {
const response = await get(`api/currentuser`)
if (!response.ok) {
dispatch(loadCurrentUserFailure({ type: null, message: 'error' }))
} else {
const json = await response.json()
dispatch(loadCurrentUserSuccess(json))
}
return response
} catch (err) {
dispatch(loadCurrentUserFailure({ type: err.name, message: err.message }))
logError(err.name, err.message)
return err
}
}
}
'get' 函数是我创建的用于处理 'fetch' GET 调用的中间件(它向 header 等添加了一些内容)。
这是我的测试:
it('create an action to load the current user', () => {
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
const store = mockStore()
const expectActions = [{ type: LOAD_CURRENT_USER_REQUEST }]
store.dispatch(actions.loadCurrentUser())
expect(store.getActions()).toEqual(expectActions)
})
我的控制台出现这个错误:
Argument of type '(dispatch: Dispatch<any>) => Promise<any>' is not assignable to parameter of type 'AnyAction'.
Property 'type' is missing in type '(dispatch: Dispatch<any>) => Promise<any>' but required in type 'AnyAction'.
我不确定我在这里做错了什么,我查看了测试异步动作创建者的 redux 示例方法,这很相似。我不知道我的问题出在哪里。
我知道我将不得不模拟我的 fetch
API 调用并更新我的 expectActions
变量,但代码因此无法编译..
解决方法如下:
index.ts
:
import fetch from 'node-fetch';
import { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
const logError = (name, message) => console.error(`name: ${name}, message: ${message}`);
const get = url => fetch(url);
export const LOAD_CURRENT_USER_REQUEST = 'LOAD_CURRENT_USER_REQUEST';
export const LOAD_CURRENT_USER_SUCCESS = 'LOAD_CURRENT_USER_SUCCESS';
export const LOAD_CURRENT_USER_FAILURE = 'LOAD_CURRENT_USER_FAILURE';
export const loadCurrentUserRequest = () => ({
type: LOAD_CURRENT_USER_REQUEST
});
export const loadCurrentUserSuccess = (payload: any) => ({
type: LOAD_CURRENT_USER_SUCCESS,
payload
});
export const loadCurrentUserFailure = (payload: any) => ({
type: LOAD_CURRENT_USER_FAILURE,
payload
});
export const loadCurrentUser = () => {
return async (dispatch: ThunkDispatch<any, any, AnyAction>) => {
dispatch(loadCurrentUserRequest());
try {
const response = await get(`api/currentuser`);
if (!response.ok) {
dispatch(loadCurrentUserFailure({ type: null, message: 'error' }));
} else {
const json = await response.json();
dispatch(loadCurrentUserSuccess(json));
}
return response;
} catch (err) {
dispatch(loadCurrentUserFailure({ type: err.name, message: err.message }));
logError(err.name, err.message);
return err;
}
};
};
单元测试:
import configureStore from 'redux-mock-store';
import thunk, { ThunkDispatch } from 'redux-thunk';
import * as actions from './';
import { AnyAction } from 'redux';
import fetch from 'node-fetch';
jest.mock('node-fetch', () => jest.fn());
const initialState = {};
type State = typeof initialState;
const middlewares = [thunk];
const mockStore = configureStore<State, ThunkDispatch<State, any, AnyAction>>(middlewares);
const store = mockStore(initialState);
describe('action creators', () => {
describe('#loadCurrentUser', () => {
afterEach(() => {
store.clearActions();
});
it('load current user success', async () => {
const userMocked = { userId: 1 };
(fetch as jest.MockedFunction<any>).mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce(userMocked)
});
await store.dispatch(actions.loadCurrentUser());
expect(fetch).toBeCalledWith('api/currentuser');
expect(store.getActions()).toEqual([
{ type: actions.LOAD_CURRENT_USER_REQUEST },
{ type: actions.LOAD_CURRENT_USER_SUCCESS, payload: userMocked }
]);
});
it('load current user failed', async () => {
(fetch as jest.MockedFunction<any>).mockResolvedValueOnce({ ok: false });
await store.dispatch(actions.loadCurrentUser());
expect(fetch).toBeCalledWith('api/currentuser');
expect(store.getActions()).toEqual([
{ type: actions.LOAD_CURRENT_USER_REQUEST },
{
type: actions.LOAD_CURRENT_USER_FAILURE,
payload: {
type: null,
message: 'error'
}
}
]);
});
it('load current user failed when fetch error', async () => {
(fetch as jest.MockedFunction<any>).mockRejectedValueOnce(new Error('fetch error'));
await store.dispatch(actions.loadCurrentUser());
expect(fetch).toBeCalledWith('api/currentuser');
expect(store.getActions()).toEqual([
{ type: actions.LOAD_CURRENT_USER_REQUEST },
{
type: actions.LOAD_CURRENT_USER_FAILURE,
payload: {
type: 'Error',
message: 'fetch error'
}
}
]);
});
});
});
100% 覆盖率的单元测试结果:
PASS src/Whosebug/54693637/index.spec.ts
action creators
#loadCurrentUser
✓ load current user success (8ms)
✓ load current user failed (1ms)
✓ load current user failed when fetch error (8ms)
console.error src/Whosebug/54693637/index.ts:3541
name: Error, message: fetch error
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 3.32s, estimated 5s
这是完成的演示:https://github.com/mrdulin/jest-codelab/tree/master/src/Whosebug/54693637
大家好,我正在尝试使用 Typescript 测试我的异步操作创建器,但我遇到了一个我无法解决的类型错误。
这是我的操作:
export const loadCurrentUserRequest = () => ({
type: LOAD_CURRENT_USER_REQUEST
})
export const loadCurrentUserSuccess = (payload: any) => ({
type: LOAD_CURRENT_USER_SUCCESS,
payload
})
export const loadCurrentUserFailure = (payload: any) => ({
type: LOAD_CURRENT_USER_FAILURE,
payload
})
这是我的异步动作创建者:
export const loadCurrentUser = () => {
return async (dispatch: Dispatch<any>) => {
dispatch(loadCurrentUserRequest())
try {
const response = await get(`api/currentuser`)
if (!response.ok) {
dispatch(loadCurrentUserFailure({ type: null, message: 'error' }))
} else {
const json = await response.json()
dispatch(loadCurrentUserSuccess(json))
}
return response
} catch (err) {
dispatch(loadCurrentUserFailure({ type: err.name, message: err.message }))
logError(err.name, err.message)
return err
}
}
}
'get' 函数是我创建的用于处理 'fetch' GET 调用的中间件(它向 header 等添加了一些内容)。
这是我的测试:
it('create an action to load the current user', () => {
const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)
const store = mockStore()
const expectActions = [{ type: LOAD_CURRENT_USER_REQUEST }]
store.dispatch(actions.loadCurrentUser())
expect(store.getActions()).toEqual(expectActions)
})
我的控制台出现这个错误:
Argument of type '(dispatch: Dispatch<any>) => Promise<any>' is not assignable to parameter of type 'AnyAction'.
Property 'type' is missing in type '(dispatch: Dispatch<any>) => Promise<any>' but required in type 'AnyAction'.
我不确定我在这里做错了什么,我查看了测试异步动作创建者的 redux 示例方法,这很相似。我不知道我的问题出在哪里。
我知道我将不得不模拟我的 fetch
API 调用并更新我的 expectActions
变量,但代码因此无法编译..
解决方法如下:
index.ts
:
import fetch from 'node-fetch';
import { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
const logError = (name, message) => console.error(`name: ${name}, message: ${message}`);
const get = url => fetch(url);
export const LOAD_CURRENT_USER_REQUEST = 'LOAD_CURRENT_USER_REQUEST';
export const LOAD_CURRENT_USER_SUCCESS = 'LOAD_CURRENT_USER_SUCCESS';
export const LOAD_CURRENT_USER_FAILURE = 'LOAD_CURRENT_USER_FAILURE';
export const loadCurrentUserRequest = () => ({
type: LOAD_CURRENT_USER_REQUEST
});
export const loadCurrentUserSuccess = (payload: any) => ({
type: LOAD_CURRENT_USER_SUCCESS,
payload
});
export const loadCurrentUserFailure = (payload: any) => ({
type: LOAD_CURRENT_USER_FAILURE,
payload
});
export const loadCurrentUser = () => {
return async (dispatch: ThunkDispatch<any, any, AnyAction>) => {
dispatch(loadCurrentUserRequest());
try {
const response = await get(`api/currentuser`);
if (!response.ok) {
dispatch(loadCurrentUserFailure({ type: null, message: 'error' }));
} else {
const json = await response.json();
dispatch(loadCurrentUserSuccess(json));
}
return response;
} catch (err) {
dispatch(loadCurrentUserFailure({ type: err.name, message: err.message }));
logError(err.name, err.message);
return err;
}
};
};
单元测试:
import configureStore from 'redux-mock-store';
import thunk, { ThunkDispatch } from 'redux-thunk';
import * as actions from './';
import { AnyAction } from 'redux';
import fetch from 'node-fetch';
jest.mock('node-fetch', () => jest.fn());
const initialState = {};
type State = typeof initialState;
const middlewares = [thunk];
const mockStore = configureStore<State, ThunkDispatch<State, any, AnyAction>>(middlewares);
const store = mockStore(initialState);
describe('action creators', () => {
describe('#loadCurrentUser', () => {
afterEach(() => {
store.clearActions();
});
it('load current user success', async () => {
const userMocked = { userId: 1 };
(fetch as jest.MockedFunction<any>).mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValueOnce(userMocked)
});
await store.dispatch(actions.loadCurrentUser());
expect(fetch).toBeCalledWith('api/currentuser');
expect(store.getActions()).toEqual([
{ type: actions.LOAD_CURRENT_USER_REQUEST },
{ type: actions.LOAD_CURRENT_USER_SUCCESS, payload: userMocked }
]);
});
it('load current user failed', async () => {
(fetch as jest.MockedFunction<any>).mockResolvedValueOnce({ ok: false });
await store.dispatch(actions.loadCurrentUser());
expect(fetch).toBeCalledWith('api/currentuser');
expect(store.getActions()).toEqual([
{ type: actions.LOAD_CURRENT_USER_REQUEST },
{
type: actions.LOAD_CURRENT_USER_FAILURE,
payload: {
type: null,
message: 'error'
}
}
]);
});
it('load current user failed when fetch error', async () => {
(fetch as jest.MockedFunction<any>).mockRejectedValueOnce(new Error('fetch error'));
await store.dispatch(actions.loadCurrentUser());
expect(fetch).toBeCalledWith('api/currentuser');
expect(store.getActions()).toEqual([
{ type: actions.LOAD_CURRENT_USER_REQUEST },
{
type: actions.LOAD_CURRENT_USER_FAILURE,
payload: {
type: 'Error',
message: 'fetch error'
}
}
]);
});
});
});
100% 覆盖率的单元测试结果:
PASS src/Whosebug/54693637/index.spec.ts
action creators
#loadCurrentUser
✓ load current user success (8ms)
✓ load current user failed (1ms)
✓ load current user failed when fetch error (8ms)
console.error src/Whosebug/54693637/index.ts:3541
name: Error, message: fetch error
----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 3.32s, estimated 5s
这是完成的演示:https://github.com/mrdulin/jest-codelab/tree/master/src/Whosebug/54693637