Как проверить сбои запросов API с помощью Redux Saga?

Я пытаюсь протестировать все сценарии, которым могла бы следовать моя сага, но я не могу добиться того, чего хочу. Это довольно просто, у меня есть HTTP-запрос (вход в систему), и я хочу проверить успешность и случаи сбоя, издевавшись над своим методом API.

Но похоже, что call effect не запускает мою функцию api, я еще не понимаю, как это работает, но я предполагаю, что промежуточное программное обеспечение отвечает за вызов функции, и поскольку я не хожу через магазин на моем тесте я не могу получить результат.

Итак, мой вопрос: как вы можете протестировать свою сагу, когда вам нужно отправить различные действия (обычно успешные или неудачные) рядом с вашим асинхронным вызовом?

Я искал пример, я нашел саги с успехом и неудачей, но случай сбоя никогда не проверяется, например, в примере корзины для покупок здесь

SAGA.JS

export function* login(action) {
  try {
    const user = yield call(api.login, action);
    return yield put(actions.loginSuccess(user));
  } catch(e) {
    yield put(actions.loginFail(e));
  }
}

export default function* rootAuthenticationSagas() {
  yield* takeLatest(LOGIN, login);
}

TEST.JS

describe('login', () => {
  context('When it fails', () => {
    before('Stub the api', () => {
      sinon.stub(api, 'login', () => {
        // IT NEVER COMES HERE !
        return Promise.reject({ error: 'user not found' });
      });
    });

    it('should return a LOGIN_FAIL action', () => {
      const action = {
        payload: {
          name: 'toto',
          password: '123456'
        }
      };
      const generator = login(action);

      // THE CALL YIELD
      generator.next();

      const expectedResult = put({ type: 'LOGIN_FAIL', payload: { error: 'user not found' } });
      expect(generator.next().value).to.be.eql(expectedResult); // FAIL BECAUSE I GET A LOGIN_SUCCESS INSTEAD OF A FAIL ONE
    });
  });
});

person Ludo    schedule 26.02.2016    source источник


Ответы (3)


Ответ Марка правильный. Промежуточное ПО выполняет эти инструкции. Но это облегчает вашу жизнь: в тесте вы можете указать все, что захотите в качестве аргумента для next(), а функция генератора получит это в результате yield. Это именно то, что делает промежуточное программное обеспечение саги (за исключением того, что оно фактически запускает запрос вместо того, чтобы дать вам поддельный ответ).

Чтобы yield получил произвольное значение, передайте его next(). Чтобы он «получил» ошибку, передайте ее throw(). В вашем примере:

it('should return a LOGIN_FAIL action', () => {
  const action = {
    payload: {
      name: 'toto',
      password: '123456'
    }
  };
  const generator = login(action);

  // Check that Saga asks to call the API
  expect(
    generator.next().value
  ).to.be.eql(
    call(api.login, action)
  );

  // Note that *no actual request was made*!
  // We are just checking that the sequence of effects matches our expectations.

  // Check that Saga reacts correctly to the failure
  expect(
    generator.throw({
      error: 'user not found'
    }).value
  ).to.be.eql(
    put({
      type: 'LOGIN_FAIL',
      payload: { error: 'user not found' }
    })
  );
});
person Dan Abramov    schedule 27.02.2016

Правильно - насколько я понимаю, весь смысл Redux-Saga в том, что ваша функция саги использует API-интерфейсы саги для возврата объектов, описывающих действие, а затем промежуточное программное обеспечение позже просматривает эти объекты для фактического выполнения поведения. Итак, оператор yield call(myApiFunction, "/someEndpoint", arg1, arg2) в саге может вернуть объект, который условно выглядит как {effectType : CALL, function: myApiFunction, params: [arg1, arg2]}.

Вы можете либо проверить источник redux-saga, чтобы точно увидеть, как эти декларативные объекты на самом деле выглядят, и создать соответствующий объект для сравнения в вашем тесте, либо использовать сами функции API для создания объектов (что я думаю, что redux-saga делает в их тестовом коде).

person markerikson    schedule 26.02.2016

Вы также можете использовать вспомогательную библиотеку для тестирования своих саг, например redux-saga-testing.

Отказ от ответственности: я написал эту библиотеку для решения той же самой проблемы

Эта библиотека сделает ваш тест похожим на любой другой (синхронный) тест, который намного проще понять, чем вызывать generator.next() вручную.

Взяв ваш пример, вы можете написать следующие тесты:

(он использует синтаксис Jest, но по сути он такой же, как и Mocha, он полностью не зависит от библиотеки)

import sagaHelper from 'redux-saga-testing';
import { call, put } from 'redux-saga/effects';
import actions from './my-actions';
import api from './your-api';

// Your example
export function* login(action) {
    try {
        const user = yield call(api.login, action);
        return yield put(actions.loginSuccess(user));
    } catch(e) {
        yield put(actions.loginFail(e.message)); // Just changed that from "e" to "e.message"
    }
}


describe('When testing a Saga that throws an error', () => {
    const it = sagaHelper(login({ type: 'LOGIN', payload: 'Ludo'}));

    it('should have called the API first, which will throw an exception', result => {
        expect(result).toEqual(call(api, { type: 'LOGIN', payload: 'Ludo'}));
        return new Error('Something went wrong');
    });

    it('and then trigger an error action with the error message', result => {
        expect(result).toEqual(put(actions.loginFail('Something went wrong')));
    });
});

describe('When testing a Saga and it works fine', () => {
    const it = sagaHelper(login({ type: 'LOGIN', payload: 'Ludo'}));

    it('should have called the API first, which will return some data', result => {
        expect(result).toEqual(call(api, { type: 'LOGIN', payload: 'Ludo'}));
        return { username: 'Ludo', email: '[email protected]' };
    });

    it('and then call the success action with the data returned by the API', result => {
        expect(result).toEqual(put(actions.loginSuccess({ username: 'Ludo', email: '[email protected]' })));
    });
});

Дополнительные примеры (с использованием Jest, Mocha и AVA) на GitHub.

person Antoine Jaussoin    schedule 19.10.2016