如何测试包含多个 API 请求和数组转换的 redux-thunk 操作?
How to test a redux-thunk action that contains multiple API requests and array transformations?
我有一个 redux-thunk 操作,其中包含多个 API-请求,这些请求将从一个端点获取数据以从不同端点获取其他相关数据,我还有几个数组转换来合并一些的数据在一起。
虽然我不确定这是否是最佳做法,但就目前而言,它满足了我的需要。但是,很难测试,因为我不确定测试它的正确方法是什么。我搜索了互联网并查看了 "thunk" 测试的许多不同变体,但到目前为止,我的每种方法都失败了。
我将非常感谢一些关于如何测试像我这样的 thunk 操作的指导,或者如果它使测试更容易的话,我可能会更好地实施我所拥有的操作。
我的 thunk-Action...
export const fetchTopStreamsStartAsync = () => {
return async dispatch => {
try {
const headers = {
'Client-ID': process.env.CLIENT_ID
};
const url = 'https://api.twitch.tv/helix/streams?first=5';
const userUrl = 'https://api.twitch.tv/helix/users?';
let userIds = '';
dispatch(fetchTopStreamsStart());
const response = await axios.get(url, { headers });
const topStreams = response.data.data;
topStreams.forEach(stream => (userIds += `id=${stream.user_id}&`));
userIds = userIds.slice(0, -1);
const userResponse = await axios.get(userUrl + userIds, { headers });
const users = userResponse.data.data;
const completeStreams = topStreams.map(stream => {
stream.avatar = users.find(
user => user.id === stream.user_id
).profile_image_url;
return stream;
});
const mappedStreams = completeStreams.map(
({ thumbnail_url, ...rest }) => ({
...rest,
thumbnail: thumbnail_url.replace(/{width}x{height}/gi, '1280x720')
})
);
dispatch(fetchTopStreamsSuccess(mappedStreams));
} catch (error) {
dispatch(fetchTopStreamsFail(error.message));
}
};
};
许多失败的测试方法之一...
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import axios from 'axios';
import moxios from 'moxios';
import {
fetchTopStreamsStart,
fetchTopStreamsSuccess,
fetchTopStreamsStartAsync
} from './streams.actions';
const mockStore = configureMockStore([thunk]);
describe('thunks', () => {
describe('fetchTopStreamsStartAsync', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
it('creates both fetchTopStreamsStart and fetchTopStreamsSuccess when api call succeeds', () => {
const responsePayload = [{ id: 1 }, { id: 2 }, { id: 3 }];
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: responsePayload
});
});
const store = mockStore();
const expectedActions = [
fetchTopStreamsStart(),
fetchTopStreamsSuccess(responsePayload)
];
return store.dispatch(fetchTopStreamsStartAsync()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
});
});
这是我在接收值失败测试中遇到的错误...
+ "payload": "Cannot read property 'forEach' of undefined",
+ "type": "FETCH_TOP_STREAMS_FAIL",
更新: 正如@mgarcia 建议的那样,我将 responsePayload
的格式从 [{ id: 1 }, { id: 2 }, { id: 3 }]
更改为 { data: [{ id: 1 }, { id: 2 }, { id: 3 }] }
,现在我不收到初始错误,但现在收到以下错误:
: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
我仍然不明白的是,测试是否必须复制多个 API 调用的确切结构,或者仅模拟一个响应就足够了?我仍在尝试找出 Async callback...
错误的原因。
您正在通过 moxios 模拟 axios 请求,但似乎您没有以预期的格式返回数据。
在您的动作创建器中,您将响应数据读为:
const topStreams = response.data.data;
const users = userResponse.data.data;
但是你在嘲笑响应,所以它 returns:
const responsePayload = [{ id: 1 }, { id: 2 }, { id: 3 }];
相反,您似乎应该返回:
const responsePayload = { data: [{ id: 1 }, { id: 2 }, { id: 3 }] };
除了模拟响应之外,您的代码还存在一些其他问题。首先,正如您自己注意到的那样,您只是在嘲笑第一个请求。您应该模拟第二个请求并返回所需的数据。其次,在您的断言中,您希望在以下位置创建操作:
const expectedActions = [
fetchTopStreamsStart(),
fetchTopStreamsSuccess(responsePayload)
];
这不是真的,因为您正在操作创建器中处理 responsePayload
,因此您在操作创建器中调用 fetchTopStreamsSuccess
的负载将不同于 responsePayload
.
考虑到所有这些因素,您的测试代码可能如下所示:
it('creates both fetchTopStreamsStart and fetchTopStreamsSuccess when api call succeeds', () => {
const streamsResponse = [
{ user_id: 1, thumbnail_url: 'thumbnail-1-{width}x{height}' },
{ user_id: 2, thumbnail_url: 'thumbnail-2-{width}x{height}' },
{ user_id: 3, thumbnail_url: 'thumbnail-3-{width}x{height}' }
];
const usersResponse = [
{ id: 1, profile_image_url: 'image-1' },
{ id: 2, profile_image_url: 'image-2' },
{ id: 3, profile_image_url: 'image-3' }
];
const store = mockStore();
// Mock the first request by URL.
moxios.stubRequest('https://api.twitch.tv/helix/streams?first=5', {
status: 200,
response: { data: streamsResponse }
});
// Mock the second request.
moxios.stubRequest('https://api.twitch.tv/helix/users?id=1&id=2&id=3', {
status: 200,
response: { data: usersResponse }
});
return store.dispatch(fetchTopStreamsStartAsync()).then(() => {
expect(store.getActions()).toEqual([
fetchTopStreamsStart(),
{
"type": "TOP_STREAMS_SUCCESS",
"payload": [
{ "avatar": "image-1", "thumbnail": "thumbnail-1-1280x720", "user_id": 1 },
{ "avatar": "image-2", "thumbnail": "thumbnail-2-1280x720", "user_id": 2 },
{ "avatar": "image-3", "thumbnail": "thumbnail-3-1280x720", "user_id": 3 },
]
}
]);
});
});
请注意,我已将 fetchTopStreamsSuccess
操作的结构构造为具有等于 TOP_STREAMS_SUCCESS
的 type
属性以及具有 payload
的属性 completeStreams
数据。您可能必须将其适应您为测试通过而创建的 fetchTopStreamsSuccess
操作的真实结构。
我有一个 redux-thunk 操作,其中包含多个 API-请求,这些请求将从一个端点获取数据以从不同端点获取其他相关数据,我还有几个数组转换来合并一些的数据在一起。
虽然我不确定这是否是最佳做法,但就目前而言,它满足了我的需要。但是,很难测试,因为我不确定测试它的正确方法是什么。我搜索了互联网并查看了 "thunk" 测试的许多不同变体,但到目前为止,我的每种方法都失败了。
我将非常感谢一些关于如何测试像我这样的 thunk 操作的指导,或者如果它使测试更容易的话,我可能会更好地实施我所拥有的操作。
我的 thunk-Action...
export const fetchTopStreamsStartAsync = () => {
return async dispatch => {
try {
const headers = {
'Client-ID': process.env.CLIENT_ID
};
const url = 'https://api.twitch.tv/helix/streams?first=5';
const userUrl = 'https://api.twitch.tv/helix/users?';
let userIds = '';
dispatch(fetchTopStreamsStart());
const response = await axios.get(url, { headers });
const topStreams = response.data.data;
topStreams.forEach(stream => (userIds += `id=${stream.user_id}&`));
userIds = userIds.slice(0, -1);
const userResponse = await axios.get(userUrl + userIds, { headers });
const users = userResponse.data.data;
const completeStreams = topStreams.map(stream => {
stream.avatar = users.find(
user => user.id === stream.user_id
).profile_image_url;
return stream;
});
const mappedStreams = completeStreams.map(
({ thumbnail_url, ...rest }) => ({
...rest,
thumbnail: thumbnail_url.replace(/{width}x{height}/gi, '1280x720')
})
);
dispatch(fetchTopStreamsSuccess(mappedStreams));
} catch (error) {
dispatch(fetchTopStreamsFail(error.message));
}
};
};
许多失败的测试方法之一...
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import axios from 'axios';
import moxios from 'moxios';
import {
fetchTopStreamsStart,
fetchTopStreamsSuccess,
fetchTopStreamsStartAsync
} from './streams.actions';
const mockStore = configureMockStore([thunk]);
describe('thunks', () => {
describe('fetchTopStreamsStartAsync', () => {
beforeEach(() => {
moxios.install();
});
afterEach(() => {
moxios.uninstall();
});
it('creates both fetchTopStreamsStart and fetchTopStreamsSuccess when api call succeeds', () => {
const responsePayload = [{ id: 1 }, { id: 2 }, { id: 3 }];
moxios.wait(() => {
const request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: responsePayload
});
});
const store = mockStore();
const expectedActions = [
fetchTopStreamsStart(),
fetchTopStreamsSuccess(responsePayload)
];
return store.dispatch(fetchTopStreamsStartAsync()).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
});
});
这是我在接收值失败测试中遇到的错误...
+ "payload": "Cannot read property 'forEach' of undefined",
+ "type": "FETCH_TOP_STREAMS_FAIL",
更新: 正如@mgarcia 建议的那样,我将 responsePayload
的格式从 [{ id: 1 }, { id: 2 }, { id: 3 }]
更改为 { data: [{ id: 1 }, { id: 2 }, { id: 3 }] }
,现在我不收到初始错误,但现在收到以下错误:
: Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Error:
我仍然不明白的是,测试是否必须复制多个 API 调用的确切结构,或者仅模拟一个响应就足够了?我仍在尝试找出 Async callback...
错误的原因。
您正在通过 moxios 模拟 axios 请求,但似乎您没有以预期的格式返回数据。
在您的动作创建器中,您将响应数据读为:
const topStreams = response.data.data;
const users = userResponse.data.data;
但是你在嘲笑响应,所以它 returns:
const responsePayload = [{ id: 1 }, { id: 2 }, { id: 3 }];
相反,您似乎应该返回:
const responsePayload = { data: [{ id: 1 }, { id: 2 }, { id: 3 }] };
除了模拟响应之外,您的代码还存在一些其他问题。首先,正如您自己注意到的那样,您只是在嘲笑第一个请求。您应该模拟第二个请求并返回所需的数据。其次,在您的断言中,您希望在以下位置创建操作:
const expectedActions = [
fetchTopStreamsStart(),
fetchTopStreamsSuccess(responsePayload)
];
这不是真的,因为您正在操作创建器中处理 responsePayload
,因此您在操作创建器中调用 fetchTopStreamsSuccess
的负载将不同于 responsePayload
.
考虑到所有这些因素,您的测试代码可能如下所示:
it('creates both fetchTopStreamsStart and fetchTopStreamsSuccess when api call succeeds', () => {
const streamsResponse = [
{ user_id: 1, thumbnail_url: 'thumbnail-1-{width}x{height}' },
{ user_id: 2, thumbnail_url: 'thumbnail-2-{width}x{height}' },
{ user_id: 3, thumbnail_url: 'thumbnail-3-{width}x{height}' }
];
const usersResponse = [
{ id: 1, profile_image_url: 'image-1' },
{ id: 2, profile_image_url: 'image-2' },
{ id: 3, profile_image_url: 'image-3' }
];
const store = mockStore();
// Mock the first request by URL.
moxios.stubRequest('https://api.twitch.tv/helix/streams?first=5', {
status: 200,
response: { data: streamsResponse }
});
// Mock the second request.
moxios.stubRequest('https://api.twitch.tv/helix/users?id=1&id=2&id=3', {
status: 200,
response: { data: usersResponse }
});
return store.dispatch(fetchTopStreamsStartAsync()).then(() => {
expect(store.getActions()).toEqual([
fetchTopStreamsStart(),
{
"type": "TOP_STREAMS_SUCCESS",
"payload": [
{ "avatar": "image-1", "thumbnail": "thumbnail-1-1280x720", "user_id": 1 },
{ "avatar": "image-2", "thumbnail": "thumbnail-2-1280x720", "user_id": 2 },
{ "avatar": "image-3", "thumbnail": "thumbnail-3-1280x720", "user_id": 3 },
]
}
]);
});
});
请注意,我已将 fetchTopStreamsSuccess
操作的结构构造为具有等于 TOP_STREAMS_SUCCESS
的 type
属性以及具有 payload
的属性 completeStreams
数据。您可能必须将其适应您为测试通过而创建的 fetchTopStreamsSuccess
操作的真实结构。