testing - 我正在努力测试这个 redux 传奇
问题描述
我对 redux-saga 比较陌生,并且很难测试这段代码:
import { normalize } from 'normalizr';
export function* normalizeResponse(denormalized, schema) {
const normalized = yield call(normalize, denormalized, schema);
return normalized;
}
export function* request(apiFn, action, schema) {
try {
yield put(requestStart({ type: action.type }));
const denormalized = yield call(apiFn, action.payload, action.meta);
const normalized = yield call(normalizeResponse, denormalized, schema);
yield put(requestSuccess({ type: action.type }));
return normalized;
} catch (e) {
if (__DEV__ && !__TEST__) {
Alert.alert('Something went wrong');
console.log(`Error in request saga: ${action.type}`, e);
}
if (action.type) {
const payload = { type: action.type, error: e };
const meta = action.payload || {};
yield put(requestFailure(payload, meta));
}
}
}
export function* photosShow() {
while (true) {
const action = yield take(t.PHOTOS_SHOW);
const normalized = yield call(request, api.show, action, {
photo: schema.photo,
});
if (normalized) yield put(setEntities(normalized));
}
}
在网上,我找到了一些 redux saga 测试包和一些教程,但似乎没有一个比基础知识多得多。以下是传奇如何运作的分步说明:
photosShow
使用 Flux 标准操作调用,有效载荷为 { id: 1}- 这将调用
request
作为实用程序函数的生成器来发出 API 请求,然后规范化响应。 - 首先,
requestStart
会触发一个动作 - 然后会调用 api 端点
- 如果成功,将触发 requestSuccess 动作
- 然后将使用 normalizr 对响应进行标准化
- 然后以redux状态存储
setEntities
(返回photosShow)
任何有关如何解决此问题的帮助将不胜感激。
解决方案
我也很难阅读所有的redux-saga
测试资源......没有找到合适的解决方案(至少,对我来说)。
我最终的结果是:
- 我用工作商店“渲染”了一个空的 React 应用程序
- 我手动触发有趣的动作(触发我正在测试的 sagas 的动作)
- 我监视 sagas 消耗的每一个外部资源
最后:我触发了传奇,我发现它们触发了其他东西。
我认为 sagas 是一个黑匣子,我检查他们是否尊重应用程序所有其他部分的合同。
我从我的身份验证 sagas 测试中举了一个例子(我知道,我打破了很多测试的好习惯,它来自我早期的 saga 测试日)(关于renderWithRedux
andspyUtil
函数,请参见下文):
describe("Login flow with valid credentials", () => {
let user = "stefano";
let pwd = "my_super_secret_password";
let app;
let spies;
// const spiedConsole = spyConsole();
beforeAll(() => {
app = renderWithRedux(<></>);
spies = {
LOGIN_SUCCESS_creator: spyUtil(authActions, "LOGIN_SUCCESS_creator"),
navigate: spyUtil(ReachRouter, "navigate"),
postLogin: spyUtil(authNetwork, "postLogin", postLoginOk),
redirectBackFromLoginPage: spyUtil(navigationData, "redirectBackFromLoginPage")
};
});
test("1 - the login API should be called as soon as the LOGIN_REQUEST action is dispatched", async () => {
app.store.dispatch(authActions.LOGIN_REQUEST_creator(user, pwd));
expect(spies.postLogin.spy).toHaveBeenCalledWith(user, pwd);
});
test("2 - then when the login API is successfull, a LOGIN_SUCCESS action should be dispatched with the tokens", async () => {
expect(spies.LOGIN_SUCCESS_creator.spy).toHaveBeenCalledWith(
expect.any(String),
expect.any(String)
);
});
test("3 - then the router should be asked to make a redirect to the initial location", async () => {
expect(spies.redirectBackFromLoginPage.spy).toHaveBeenCalled();
expect(spies.navigate.spy).toHaveBeenCalledWith(expect.stringMatching(/\//));
});
afterAll(() => {
spies.values().forEach(obj => obj.spy.mockRestore());
// spiedConsole.mockRestore();
cleanup();
});
});
一步一步: - 我用一个正常工作的 Redux+Saga 商店渲染一个空的应用程序
app = renderWithRedux(<></>);
- 我窥探传奇之外的一切
spies = {
LOGIN_SUCCESS_creator: spyUtil(authActions, "LOGIN_SUCCESS_creator"),
navigate: spyUtil(ReachRouter, "navigate"),
postLogin: spyUtil(authNetwork, "postLogin", postLoginOk),
redirectBackFromLoginPage: spyUtil(navigationData, "redirectBackFromLoginPage")
};
在哪里:
LOGIN_SUCCESS_creator
是动作创造者navigate
来自到达路由器postLogin
发出 AJAX 请求(用一个假函数模拟它,几乎立即返回一个“成功”响应(但解决一个承诺))redirectBackFromLoginPage
是一个再次消耗(在某些条件下)navigate
实用程序的函数- 我触发了该
LOGIN_REQUEST
操作,并且我希望已使用正确的凭据调用了 AJAX 触发函数
- 我触发了该
test("1 - the login API should be called as soon as the LOGIN_REQUEST action is dispatched", async () => {
app.store.dispatch(authActions.LOGIN_REQUEST_creator(user, pwd));
expect(spies.postLogin.spy).toHaveBeenCalledWith(user, pwd);
});
- 我检查该
LOGIN_SUCCESS
操作是否将与身份验证令牌一起发送
test("2 - then when the login API is successfull, a LOGIN_SUCCESS action should be dispatched with the tokens", async () => {
expect(spies.LOGIN_SUCCESS_creator.spy).toHaveBeenCalledWith(
expect.any(String),
expect.any(String)
);
});
- 我检查是否使用正确的路由调用了路由器(
/
对于主页)
test("3 - then the router should be asked to make a redirect to the initial location", async () => {
expect(spies.redirectBackFromLoginPage.spy).toHaveBeenCalled();
expect(spies.navigate.spy).toHaveBeenCalledWith(expect.stringMatching(/\//));
});
- 然后,我清除一切
afterAll(() => {
spies.values().forEach(obj => obj.spy.mockRestore());
// spiedConsole.mockRestore();
cleanup();
});
这是“我的”(来自 Kent C. Dodds)renderWithRedux
函数
// @see https://github.com/kentcdodds/react-testing-library/blob/master/examples/__tests__/react-redux.js
export function renderWithRedux(ui, { initialState, store = configureStore() } = {}) {
return {
...render(
<div>
<Provider store={store}>{ui}</Provider>
</div>
),
// adding `store` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
store
};
}
我的一个函数在哪里configureStore
用各种中间件构建整个 Redux 存储。
这是我的spyUtil
功能
/**
* A all-in-one spy and mock function
* @param {object} obj
* @param {string} name
* @param {function} mockFunction
*/
export function spyUtil(obj, name, mockFunction = undefined) {
const spy = jest.spyOn(obj, name);
let mock;
if (mockFunction) {
mock = jest.fn(mockFunction);
obj[name].mockImplementation(mock);
}
return { spy, mock };
}
请注意,这只是身份验证流程之一,我没有在这里报告所有案例。
我想知道你对此的看法
推荐阅读
- python - 正在测试的模型名在 Django 中未定义错误
- reactjs - 输入缺少对每个更改的关注
- redis - Redis,实时用例
- apache-nifi - 在nifi jolt中将字符串拆分为数组
- python - replace() 不接受关键字参数
- angular - 从 Firebase 身份验证中检测用户
- javascript - 尽管尝试了不同的调用语法,Javascript Bootstrap 表仍未加载数据
- firebase - react-native-firebase v6 - Android - 尽管不应该显示通知
- javascript - 无法在 Android 中获取 cookie
- python - 为一个简单的 Python 程序创建一个循环