Как протестировать внутреннюю наблюдаемую, которая не будет завершена?

Я использую jest для тестирования эпика redux-observable, который разветвляется от внутреннего наблюдаемого объекта, созданного с помощью Observable.fromEvent, и прослушивает определенное нажатие клавиши перед выполнением действия.

Я изо всех сил пытаюсь проверить, когда внутренний Observable не получает это конкретное нажатие клавиши и, следовательно, не выполняет действие.

Используя шутку, следующие тайм-ауты:

import { Observable, Subject } from 'rxjs'
import { ActionsObservable } from 'redux-observable'
import keycode from 'keycode'

const closeOnEscKeyEpic = action$ =>
    action$.ofType('LISTEN_FOR_ESC').switchMapTo(
        Observable.fromEvent(document, 'keyup')
            .first(event => keycode(event) === 'esc')
            .mapTo({ type: 'ESC_PRESSED' })
    )

const testEpic = ({ setup, test, expect }) =>
    new Promise(resolve => {
        const input$ = new Subject()
        setup(new ActionsObservable(input$))
            .toArray()
            .subscribe(resolve)
        test(input$)
    }).then(expect)

// This times out
it('no action emitted if esc key is not pressed', () => {
    expect.assertions(1)
    return testEpic({
        setup: input$ => closeOnEscKeyEpic(input$),
        test: input$ => {
            // start listening
            input$.next({ type: 'LISTEN_FOR_ESC' })

            // press the wrong keys
            const event = new KeyboardEvent('keyup', {
                keyCode: keycode('p'),
            })
            const event2 = new KeyboardEvent('keyup', {
                keyCode: keycode('1'),
            })
            global.document.dispatchEvent(event)
            global.document.dispatchEvent(event2)

            // end test
            input$.complete()
        },
        expect: actions => {
            expect(actions).toEqual([])
        },
    })
})

Я ожидал, что вызов input$.complete() приведет к разрешению промиса в testEpic, но для этого теста это не так.

Я чувствую, что что-то упускаю. Кто-нибудь понимает, почему это не работает?


person Pete    schedule 16.09.2017    source источник


Ответы (1)


Я все еще новичок в Rx/RxJS, поэтому приношу свои извинения, если терминология этого ответа неверна. Однако я смог воспроизвести ваш сценарий.

Внутренний наблюдаемый (Observable.fromEvent) блокирует внешний наблюдаемый. Завершенное событие на вашем ActionsObservable не распространяется до тех пор, пока не будет завершено внутреннее наблюдаемое.

Попробуйте следующий фрагмент кода с этим тестовым скриптом:

  1. Запустите фрагмент кода.
  2. Press a non-Escape key.
    • Nothing should be printed to the console.
  3. Выберите «Слушать побег!» кнопка.
  4. Press a non-Escape key.
    • The keyCode should be printed to the console.
  5. Выберите «Завершить!» кнопка.
  6. Press a non-Escape key.
    • The keyCode should be printed to the console.
  7. Press the Escape key.
    • The keyCode should be printed to the console
    • Обратный вызов onNext должен вывести на консоль действие ESC_PRESSED.
    • Обратный вызов onComplete должен выводить на консоль.

document.getElementById('complete').onclick = onComplete
document.getElementById('listenForEsc').onclick = onListenForEsc

const actions = new Rx.Subject()

const epic = action$ =>
  action$.pipe(
    Rx.operators.filter(action => action.type === 'LISTEN_FOR_ESC'),
    Rx.operators.switchMapTo(
      Rx.Observable.fromEvent(document, 'keyup').pipe(
        Rx.operators.tap(event => { console.log('keyup: %s', event.keyCode) }),
        Rx.operators.first(event => event.keyCode === 27), // escape
        Rx.operators.mapTo({ type: 'ESC_PRESSED' }),
      )
    )
  )

epic(actions.asObservable()).subscribe(
  action => { console.log('next: %O', action) },
  error => { console.log('error: %O', error) },
  () => { console.log('complete') },
)

function onListenForEsc() {
  actions.next({ type: 'LISTEN_FOR_ESC' })
}

function onComplete() {
  actions.complete()
}
<script src="https://unpkg.com/[email protected]/bundles/Rx.min.js"></script>
<button id="complete">Complete!</button>
<button id="listenForEsc">Listen for Escape!</button>

Ни мраморная диаграмма switchMapTo , ни ее текстовая документация) четко указывают, что происходит, когда исходная наблюдаемая завершается раньше внутренней наблюдаемой. Однако приведенный выше фрагмент кода демонстрирует именно то, что вы наблюдали в тесте Jest.

switchMapTo мраморная диаграмма

Я считаю, что это отвечает на ваш вопрос «почему», но я не уверен, что у меня есть четкое решение для вас. Одним из вариантов может быть подключить действие отмены и использовать takeUntil для внутреннее наблюдаемое. Но это может показаться неудобным, если это когда-либо использовалось только в вашем тесте Jest.

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

person seniorquico    schedule 22.10.2017