Я изо всех сил пытаюсь протестировать эту сагу о сокращении

#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 Router
  • 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();
});
  

Это «моя» (она исходит от Кента К. Доддса) 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 };
}
  

Пожалуйста, обратите внимание, что это всего лишь один из потоков аутентификации, я не сообщил здесь обо всех случаях.

Я хотел бы знать ваши мысли по этому поводу 😉