Search code examples
typescriptjestjsgraphql-subscriptionssetimmediate

Jest: await vs setImmediate vs useFakeTimers vs new Promise(setImmediate)


What follows is a Jest test in TypeScript. I'm wondering why setImmediate() is required.

The first example is a test that works. Next are various things I've tried that don't work. I'm not understanding the what is going on. The signature for pubsub.publish is: (method) PubSub.publish(triggerName: string, payload: any): Promise<void>

  test.only('subscriptions', async () => {
    const document = parse(`
      subscription {
        create 
      }
    `)

    const sub = <AsyncIterator<ExecutionResult>>await subscribe(schema, document);

    expect(sub.next).toBeDefined()

    // setInterval and process.nextTick also work here:
    setImmediate(() => pubsub.publish('CREATE_ONE', { create: "FLUM!" }))  // this works


    const { value: { errors, data } } = await sub.next()

    expect(errors).toBeUndefined()
    expect(data).toBeDefined()
    expect(data.create).toBe('FLUM!')
  }, 10000)

So these are other things I've tried, some after researching answers to similar issues. All these attempts fail with a timeout exception on the test:



  test.only('subscriptions', async () => {
  // attempt #1: jest.useFakeTimers()

    const document = parse(`
      subscription {
        create 
      }
    `)

    const sub = <AsyncIterator<ExecutionResult>>await subscribe(schema, document);

    expect(sub.next).toBeDefined()

    // #1, cont: 
    // pubsub.publish('CREATE_ONE', { create: "FLUM!" })
    // or...
    // await pubsub.publish('CREATE_ONE', { create: "FLUM!" })
    // this works, though, like in previous test, but with fake timers:
    // setImmediate(() => pubsub.publish('CREATE_ONE', { create: "FLUM!" }))


    // attempt #2:
    // await pubsub.publish('CREATE_ONE', { create: "FLUM!" })

    // attempt #3:
    // pubsub.publish('CREATE_ONE', { create: "FLUM!" })
    // await new Promise(setImmediate)

    // attempt #3a (variant):
    // await new Promise((resolve) => setImmediate(resolve));

    const { value: { errors, data } } = await sub.next()

    expect(errors).toBeUndefined()
    expect(data).toBeDefined()
    expect(data.create).toBe('FLUM!')
  }, 10000)

I understand that setImmediate puts a function in the event loop to be executed immediately after any pending I/O events. I'm not sure why it is needed, because pubsub.publish() returns a Promise that can be handled with an await, but what happens in that case is that the next line, await sub.next() is never called.

My thinking is that there's a setInterval call being made in pubsub.publish(), and setImmediate waits for any pending setInterval events to complete (my understanding is fuzzy on this). Attempts 3 and 3a are mechanisms I found elsewhere to do this, but they don't seem to work in this case.

The question: Why does this test require setImmediate to pass?


Solution

  • So my confusion is due to what setImmediate does and doesn't do. This is what is happening:

        // setInterval and process.nextTick also work here:
        setImmediate(() => pubsub.publish('CREATE_ONE', { create: "FLUM!" })) 
        const { value: { errors, data } } = await sub.next()
    

    Without the setImmediate(), the publish event is sent before sub.next() is called, so it is not captured. You might think that setImmediate (or process.nextTick) would cause immediate execution of the publish function, but nope. Instead, setImmediate delays the publish call long enough for sub.next() to execute.

    I am now going to do some remedial reading of how setImmediate and process.nextTick actually work.