如何测试一个无法完成的内部Observable?

时间:2017-09-16 02:35:00

标签: rxjs jest redux-observable

我正在使用jest来测试一个redux-observable史诗,它会使用Observable.fromEvent创建一个内部可观察对象,并在发出动作之前侦听特定的按键。

我正在努力测试内部Observable何时没有收到此特定按键,因此不会发出动作。

使用jest,以下时间超时:

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中的承诺解决,但对于此测试则不会。

我觉得我错过了什么。有谁知道为什么这不起作用?

1 个答案:

答案 0 :(得分:1)

我还是Rx / RxJS的新手,所以如果这个答案的术语已经关闭,我很抱歉。不过,我能够重现你的场景。

内部可观察对象(Observable.fromEvent)阻挡了外部可观察对象。 ActionsObservable上已完成的事件在内部可观察完成之后才会传播。

使用此测试脚本试用以下代码段:

  1. 运行代码段。
  2. 按非转义键。
    • 不应该在控制台上打印任何内容。
  3. 选择“倾听逃生!”按钮。
  4. 按非转义键。
    • 应将keyCode打印到控制台。
  5. 选择“完成!”按钮。
  6. 按非转义键。
    • 应将keyCode打印到控制台。
  7. 按退出键。
    • keyCode应打印到控制台
    • onNext回调应该将ESC_PRESSED操作打印到控制台。
    • onComplete回调应该打印到控制台。
  8. 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/rxjs@5.5.0/bundles/Rx.min.js"></script>
    <button id="complete">Complete!</button>
    <button id="listenForEsc">Listen for Escape!</button>

    switchMapTo大理石图nor its textual documentation)都没有清楚地表明当源观察源在内部可观察面之前完成时会发生什么。但是,上面的代码片段完全展示了您在Jest测试中观察到的内容。

    switchMapTo marble diagram

    我相信这会回答你的“为什么”的问题,但我不确定我有一个明确的解决方案。一个选项可以是hook in a cancellation action,并在内部可观察对象上使用takeUntil。但是,如果只在你的Jest测试中使用它,那可能会感到很尴尬。

    我可以看到这个史诗/模式在真实应用中不会出现问题,因为通常情况下,史诗会被创建并订阅一次而不会被取消订阅。但是,根据具体情况(例如,在单个应用程序中多次创建/销毁存储),我可以看到这会导致挂起的订阅和潜在的内存泄漏。很好记住!