#testing #redux #react-redux #redux-saga
#тестирование #сокращение #реагировать-сокращение #redux-saga
Вопрос:
Я относительно новичок в 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__ amp;amp; !__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
- И затем сохраняется в состоянии сокращения с
setEntities
помощью (обратно в photosShow)
Любая помощь в продвижении вперед с тем, как это сделать, была бы очень признательна.
Комментарии:
1. Алекс? Вы прочитали мой ответ? 😊
Ответ №1:
Я тоже изо всех сил пытался прочитать все redux-saga
ресурсы для тестирования … не найдя подходящего решения (по крайней мере, для меня).
В итоге я получил:
- Я «визуализирую» пустое приложение React с работающим хранилищем
- Я вручную запускаю интересные действия (те, которые запускают саги, которые я тестирую)
- Я слежу за каждым внешним ресурсом, используемым сагами
В конце концов: я запускаю саги и вижу, что они запускают другие вещи.
Я считаю саги черным ящиком и проверяю, соблюдают ли они контракт со всеми другими частями приложения.
Я беру пример из своих тестов аутентификации sagas (я знаю, что я нарушил много хороших методов тестирования, это связано с моими ранними днями тестирования с saga) (см. Ниже Функции renderWithRedux
и spyUtil
):
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
поступает от Reach RouterpostLogin
выполняет запрос 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();
});
Это «моя» (она исходит от Кента К. Доддса) 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 };
}
Пожалуйста, обратите внимание, что это всего лишь один из потоков аутентификации, я не сообщил здесь обо всех случаях.
Я хотел бы знать ваши мысли по этому поводу 😉