1

I'm trying to write a test for the sad path of this function:

const awaitFirstStreamForPage = async page => {
  try {
    await page.waitForSelector('[data-stream="true"]', {
      timeout: MAX_DELAY_UNTIL_FIRST_STREAM,
    })
  } catch (e) {
    throw new Error(`no stream found for ${MAX_DELAY_UNTIL_FIRST_STREAM}ms`)
  }
}

I managed to write a test that passes, but it takes 10 seconds to run because it actually waits for the test to finish.

describe('awaitFirstStreamForPage()', () => {
  it('given a page and no active stream appearing: should throw', async () => {
    jest.setTimeout(15000)

    const browser = await puppeteer.launch({ headless: true })
    const page = await getPage(browser)

    let error

    try {
      await awaitFirstStreamForPage(page)
    } catch (err) {
      error = err
    }

    const actual = error.message
    const expected = 'no stream found for 10000ms'

    expect(actual).toEqual(expected)

    await browser.close()
    jest.setTimeout(5000)
  })
})

There is probably a way to solve it using Jest's fake timers, but I couldn't get it to work. Here is my best attempt:

const flushPromises = () => new Promise(res => process.nextTick(res))

describe('awaitFirstStreamForPage()', () => {
  it('given a page and no active stream appearing: should throw', async () => {
    jest.useFakeTimers()

    const browser = await puppeteer.launch({ headless: true })
    const page = await getPage(browser)

    let error

    try {
      awaitFirstStreamForPage(page)
      jest.advanceTimersByTime(10000)
      await flushPromises()
    } catch (err) {
      error = err
    }

    const actual = error.message
    const expected = 'no stream found for 10000ms'

    expect(actual).toEqual(expected)

    await browser.close()
    jest.useRealTimers()
  })
})

which fails and throws with

(node:9697) UnhandledPromiseRejectionWarning: Error: no stream found for 10000ms

Even though I wrapped the failing function in a try/catch. How do you test a function like this using fake timers?

J. Hesters
  • 13,117
  • 31
  • 133
  • 249

2 Answers2

2

It's impossible to catch a rejection from awaitFirstStreamForPage(page) with try..catch if it's not awaited.

A rejection should be caught but after calling advanceTimersByTime and potentially after flushPromises.

It can be:

const promise = awaitFirstStreamForPage(page);
promise.catch(() => { /* suppress UnhandledPromiseRejectionWarning */ });

jest.advanceTimersByTime(10000)
await flushPromises();
await expect(promise).rejects.toThrow('no stream found for 10000ms');
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
0

The problem doesn’t seem to be the use of fake timers: the error you expected is the one being thrown. However, when testing functions that throw errors in Jest, you should wrap the error-throwing code in a function, like this:

expect(()=> {/* code that will throw error */}).toThrow()

More details here: https://jestjs.io/docs/en/expect#tothrowerror

Edit: For an async function, you should use rejects before toThrow; see this example: Can you write async tests that expect toThrow?

daniloxxv
  • 817
  • 6
  • 10
  • `toThrow` catches synchronous errors. It should be `rejects.toThrow`. And it's not enough here because operations (advanceTimersByTime) need to be performed simultaneously. Fake timers are used here to make the test shorter than 10s. – Estus Flask Sep 24 '20 at 09:52