Как я могу протестировать Observable.ajax (редукционно-наблюдаемый)?

Последние несколько дней я играл с rxjs и redux-observable и изо всех сил пытался найти способ проверить Observable.ajax. У меня есть следующая эпопея, которая создает запрос на https://jsonplaceholder.typicode.com/,

export function testApiEpic (action$) {
  return action$.ofType(REQUEST)
    .switchMap(action =>
      Observable.ajax({ url, method })
        .map(data => successTestApi(data.response))
        .catch(error => failureTestApi(error))
        .takeUntil(action$.ofType(CLEAR))
    )
}

куда,

export const REQUEST = 'my-app/testApi/REQUEST'
export const SUCCESS = 'my-app/testApi/SUCCESS'
export const FAILURE = 'my-app/testApi/FAILURE'
export const CLEAR = 'my-app/testApi/CLEAR'

export function requestTestApi () {
  return { type: REQUEST }
}
export function successTestApi (response) {
  return { type: SUCCESS, response }
}
export function failureTestApi (error) {
  return { type: FAILURE, error }
}
export function clearTestApi () {
  return { type: CLEAR }
}

Код отлично работает при запуске в браузере, но не при тестировании с помощью Jest.

я пробовал,

1) Создайте тест на основе https://redux-observable.js.org/docs/recipes/WritingTests.html. store.getActions() возвращает только {type: REQUEST}.

const epicMiddleware = createEpicMiddleware(testApiEpic)
const mockStore = configureMockStore([epicMiddleware])

describe.only('fetchUserEpic', () => {
  let store

  beforeEach(() => {
    store = mockStore()
  })

  afterEach(() => {
    epicMiddleware.replaceEpic(testApiEpic)
  })

  it('returns a response, () => {
    store.dispatch({ type: REQUEST })
    expect(store.getActions()).toEqual([
      { type: REQUEST },
      { type: SUCCESS, response }
    ])
  })
})

2) Создайте тест на основе Redux-observable: неудачный шуточный тест для эпичности. Он возвращается с

Тайм-аут — асинхронный обратный вызов не был вызван в течение тайм-аута, указанного jasmine.DEFAULT_TIMEOUT_INTERVAL.

  it('returns a response', (done) => {
    const action$ = ActionsObservable.of({ type: REQUEST })
    const store = { getState: () => {} }
    testApiEpic(action$, store)
      .toArray()
      .subscribe(actions => {
        expect(actions).to.deep.equal([
          { type: SUCCESS, response }
        ])
        done()
      })
  })

Может ли кто-нибудь указать мне, как правильно протестировать Observable.ajax?


person binkpitch    schedule 26.09.2017    source источник


Ответы (2)


Я бы последовал второму примеру из StackOverflow. Чтобы заставить его работать, вам нужно будет внести некоторые незначительные корректировки. Вместо того, чтобы импортировать Observable.ajax в ваш эпический файл и напрямую использовать эту ссылку, вам нужно использовать некоторую форму внедрения зависимостей. Один из способов — предоставить его промежуточному программному обеспечению при его создании.

import { ajax } from 'rxjs/observable/dom/ajax';

const epicMiddleware = createEpicMiddleware(rootEpic, {
  dependencies: { ajax }
});

Объект, который мы передали как dependencies, будет передан всем эпикам в качестве третьего аргумента.

export function testApiEpic (action$, store, { ajax }) {
  return action$.ofType(REQUEST)
    .switchMap(action =>
      ajax({ url, method })
        .map(data => successTestApi(data.response))
        .catch(error => failureTestApi(error))
        .takeUntil(action$.ofType(CLEAR))
    );
}

В качестве альтернативы вы можете не использовать опцию dependencies промежуточного программного обеспечения и вместо этого просто использовать параметры по умолчанию:

export function testApiEpic (action$, store, ajax = Observable.ajax) {
  return action$.ofType(REQUEST)
    .switchMap(action =>
      ajax({ url, method })
        .map(data => successTestApi(data.response))
        .catch(error => failureTestApi(error))
        .takeUntil(action$.ofType(CLEAR))
    );
}

Независимо от того, что вы выберете, когда мы тестируем эпос, мы теперь можем вызывать его напрямую и создавать для него собственный макет. Вот примеры путей успеха/ошибки/отмены Они не проверены и могут иметь проблемы, но должны дать вам общее представление

it('handles success path', (done) => {
  const action$ = ActionsObservable.of(requestTestApi())
  const store = null; // not used by epic
  const dependencies = {
    ajax: (url, method) => Observable.of({ url, method })
  };

  testApiEpic(action$, store, dependencies)
    .toArray()
    .subscribe(actions => {
      expect(actions).to.deep.equal([
        successTestApi({ url: '/whatever-it-is', method: 'WHATEVERITIS' })
      ])

      done();
    });
});

it('handles error path', (done) => {
  const action$ = ActionsObservable.of(requestTestApi())
  const store = null; // not used by epic
  const dependencies = {
    ajax: (url, method) => Observable.throw({ url, method })
  };

  testApiEpic(action$, store, dependencies)
    .toArray()
    .subscribe(actions => {
      expect(actions).to.deep.equal([
        failureTestApi({ url: '/whatever-it-is', method: 'WHATEVERITIS' })
      ])

      done();
    });
});

it('supports cancellation', (done) => {
  const action$ = ActionsObservable.of(requestTestApi(), clearTestApi())
  const store = null; // not used by epic
  const dependencies = {
    ajax: (url, method) => Observable.of({ url, method }).delay(100)
  };
  const onNext = chai.spy();

  testApiEpic(action$, store, dependencies)
    .toArray()
    .subscribe({
      next: onNext,
      complete: () => {
        onNext.should.not.have.been.called();        
        done();
      }
    });
});
person jayphelps    schedule 26.09.2017

Для первого способа:

Во-первых, используйте isomorphic-fetch вместо Observable.ajax для поддержки nock, например

const fetchSomeData = (api: string, params: FetchDataParams) => {
const request = fetch(`${api}?${stringify(params)}`)
  .then(res => res.json());
  return Observable.from(request);
};

Итак, мой эпик:

const fetchDataEpic: Epic<GateAction, ImGateState> = action$ =>
  action$
    .ofType(FETCH_MODEL)
    .mergeMap((action: FetchModel) =>
      fetchDynamicData(action.url, action.params)
        .map((payload: FetchedData) => fetchModelSucc(payload.data))
        .catch(error => Observable.of(
          fetchModelFail(error)
      )));

Затем вам может понадобиться перерыв, чтобы решить, когда закончить тест.

describe("epics", () => {
  let store: MockStore<{}>;
  beforeEach(() => {
    store = mockStore();
  });
  afterEach(() => {
    nock.cleanAll();
    epicMiddleware.replaceEpic(epic);
  });
  it("fetch data model succ", () => {
    const payload = {
      code: 0,
      data: someData,
      header: {},
      msg: "ok"
    };
    const params = {
      data1: 100,
      data2: "4"
    };
    const mock = nock("https://test.com")
      .get("/test")
      .query(params)
      .reply(200, payload);
    const go = new Promise((resolve) => {
      store.dispatch({
        type: FETCH_MODEL,
        url: "https://test.com/test",
        params
      });
      let interval: number;
      interval = window.setInterval(() => {
        if (mock.isDone()) {
          clearInterval(interval);
          resolve(store.getActions());
        }
      }, 20);
    });
    return expect(go).resolves.toEqual([
      {
        type: FETCH_MODEL,
        url: "https://test.com/assignment",
        params
      },
      {
        type: FETCH_MODEL_SUCC,
        data: somData
      }
    ]);
  });
});

наслаждайся этим :)

person Mirone    schedule 10.05.2018