Действие при ошибке тестирования - мрамор - ngrx Effects
У меня проблема с проверкой неудачного действия над моими эффектами.
Чтобы дать немного контекста, здесь эффект loadProducts выполняется при вызове действия Load. Внутри эффекта выполняется HTTP-запрос, в случае успешного выполнения этого запроса вызывается действие LoadSuccess, в противном случае вызывается LoadFail. Код здесь ниже
@Effect()
loadProducts$ = this.actions$.pipe(
ofType(productActions.ProductActionTypes.Load),
mergeMap((action: productActions.Load) =>
this.productService.getProducts().pipe(
map((products: Product[]) => (new productActions.LoadSuccess(products))),
catchError(error => of(new productActions.LoadFail(error)))
))
);
Чтобы проверить этот эффект, я использовал jest-marbles, которые почти такие же, как jasmine-marbles, так или иначе, я создал действие Load как горячее наблюдаемое, мой HTTP-ответ как холодный и ожидаемый результат по умолчанию.
it('should return a LoadFail action, with an error, on failure', () => {
const action = new Load();
const errorMessage = 'Load products fail';
const outcome = new LoadFail(errorMessage);
actions$ = hot('-a', { a: action});
const response = cold('-#|', {}, errorMessage);
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome });
expect(effects.loadProducts$).toBeObservable(expected);
});
Когда я запускаю тест, выдается ошибка, в которой говорится, что мой loadProducts наблюдаемый, и ожидаемый результат не совпадает.
✕ should return a LoadFail action, with an error, on failure (552ms)
Product effects › loadProducts › should return a LoadFail action, with an error, on failure
expect(received).toBeNotifications(expected)
Expected notifications to be:
[{"frame": 20, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"payload": "Load products fail", "type": "[Product] Load Fail"}}}, {"frame": 20, "notification": {"error": undefined, "hasValue": false, "kind": "C", "value": undefined}}]
But got:
[{"frame": 20, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"payload": "Load products fail", "type": "[Product] Load Fail"}}}]
Difference:
- Expected
+ Received
Array [
Object {
"frame": 20,
"notification": Notification {
"error": undefined,
"hasValue": true,
"kind": "N",
"value": LoadFail {
"payload": "Load products fail",
"type": "[Product] Load Fail",
},
},
},
- Object {
- "frame": 20,
- "notification": Notification {
- "error": undefined,
- "hasValue": false,
- "kind": "C",
- "value": undefined,
- },
- },
]
Я знаю, что это за ошибка, но не знаю, как ее решить. Я знаю о мире испытаний шариков
2 ответа
Я хотел бы объяснить, почему это вообще не сработало.
Как вы знаете, когда вы тестируете наблюдаемые с помощью мраморных диаграмм, вы используете не реальное время, а виртуальное время. Виртуальное время можно измерить вframes
. Значение кадра может варьироваться (например,10
, 1
), но независимо от значения, это то, что помогает проиллюстрировать ситуацию, с которой вы имеете дело.
Например, с hot(--a---b-c)
, вы описываете наблюдаемую, которая будет выдавать следующие значения: a
в 2u
, b
в 6u
а также c
в 8u
(u
- единицы времени).
Внутри RxJs создает очередь действий, и задача каждого действия - выдать значение, которое ему было присвоено.{n}u
описывает, когда действие выполнит свою задачу.
За hot(--a---b-c)
, очередь действий будет выглядеть (примерно) так:
queue = [
{ frame: '2u', value: 'a' }/* aAction */,
{ frame: '6u', value: 'b' }/* bAction */,
{ frame: '8u', value: 'c' }/* cAction */
]
hot
а также cold
при вызове создает экземпляр hot
а также cold
наблюдаемые, соответственно. Их базовый класс расширяетObservable
класс.
Теперь очень интересно посмотреть, что происходит, когда вы имеете дело с внутренними наблюдаемыми, как в вашем примере:
actions$ = hot('-a', { a: action}); // 'a' - emitted at frame 1
const response = cold('-#|', {}, errorMessage); // Error emitted at 1u after it has been subscribed
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome }); // `b` and `complete` notification, both at frame 2
В response
наблюдаемый подписан из-за a
, что означает, что уведомление об ошибке будет отправлено в frame of a
+ original frame
. То есть,frame 1
(a
прибытие) + frame1
(при появлении ошибки) = frame 2
.
Итак, почему hot('-a')
не работает?
Это из-за того, как mergeMap
обрабатывает вещи. Когда используешьmergeMap
и его братьев и сестер, если источник завершается, но оператор имеет внутренние наблюдаемые, которые все еще активны (еще не завершены), полное уведомление источника не будет передано. Это произойдет только тогда, когда будут завершены все внутренние наблюдаемые.
С другой стороны, если все внутренние наблюдаемые объекты завершены, а источник - нет, полное уведомление не будет передано следующему подписчику в цепочке. Поэтому изначально это не сработало.
Теперь давайте посмотрим, почему это работает именно так:
actions$ = hot('-a|', { a: action});
const response = cold('-#|)', {}, errorMessage);
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome });
в очереди действия пользователя будет выглядеть следующим образом:
queue = [
{ frame: '1u', value: 'a' },
{ frame: '2u', completeNotif: true },
]
когда a
получено, response
будет подписан, и поскольку это наблюдаемое, созданное с помощью cold()
, его уведомления нужно будет назначить действиям и поставить в очередь соответственно.
После response
был подписан, очередь будет выглядеть так:
queue = [
// `{ frame: '1u', value: 'a' },` is missing because when an action's task is done
// the action itself is removed from the queue
{ frame: '2u', completeNotif: true }, // Still here because the first action didn't finish
{ frame: '2u', errorNotif: true, name: 'Load products fail' }, // ' from '-#|'
{ frame: '3u', completeNotif: true },// `|` from '-#|'
]
Обратите внимание, что если в одном кадре должно быть выполнено 2 действия очереди, приоритет будет иметь самое старое.
Из вышеизложенного мы можем сказать, что источник выдаст полное уведомление до того, как внутренний наблюдаемый объект выдаст ошибку, что означает, что когда внутренний наблюдаемый объект выдаст значение, полученное в результате обнаружения ошибки (outcome
), mergeMap
пройдет полное уведомление.
В заключение, (b|)
необходимо в cold('--(b|)', { b: outcome });
потому что наблюдаемое, которое catchError
подписывается на, of(new productActions.LoadFail(error)))
, выдаст и завершится в одном кадре. Текущий кадр содержит значение кадра текущего выбранного действия. В этом случае2
, из { frame: '2u', errorNotif: true, name: 'Load products fail' }
.
Я нашел способ решить свою проблему, не уверен, что это лучший способ сделать это, но в основном я добавил канал, чтобы завершить горячее наблюдение. Пожалуйста, дайте мне знать, если есть другое решение.
it('should return a LoadFail action, with an error, on failure', () => {
const action = new Load();
const errorMessage = 'Load products fail';
const outcome = new LoadFail(errorMessage);
actions$ = hot('-a|', { a: action});
const response = cold('-#|)', {}, errorMessage);
productServiceMock.getProducts = jest.fn(() => response);
const expected = cold('--(b|)', { b: outcome });
expect(effects.loadProducts$).toBeObservable(expected);
});