3

I'm trying to write a test in nodeJS using Jest for a function that calls two async functions one after the other. I want to delay funcA, expect funcB to not be called, then run the timer down and expect funcB to be called.

The code looks something like this

//module1.js
async function mainFunc() {
  await module2.funcA()
  await module2.funcB()
}



//module2.js
async function funcA(){
  // making some async operation
}

async function funcB(){
  // making some async operation
}

I've tryed mocking the implementation of funcA like this:

const spyOnFuncA = jest.spyOn(module2, 'funcA').mockImplementation(async () => new Promise((r) => setTimeout(r, 1000)))

then in the test doing something like this:

  test('Should not call second function until first function resolved', async () => {
    jest.useFakeTimers()
    const spyOnFuncA = jest.spyOn(module2, 'funcA').mockImplementation(async () => new Promise((r) => setTimeout(r, 1000)))
    const spyOnFuncB = jest.spyOn(module2, 'funcB').mockImplementation()

    mainFunc()
    expect(spyOnFuncA).toBeCalled()
    expect(spyOnFuncB).not.toBeCalled()
    
    jest.runAllTimers()
    expect(spyOnFuncB).toBeCalled()
    
  })

I think the problem here is that the jest.useFakeTimers contradicts with the setTimeout inside the mockImplementation

Any ideas how should I test this?

Would appreciate any idea

Cheers!

tosh
  • 61
  • 1
  • 7

2 Answers2

1

Mock funcA to return a deferred promise, to be resolved later. I know sinon provides a promise helper to cover deferred so jest may include a similar construct. Otherwise here is one of the simple implementations from that answer:

class Deferred {
  constructor() {
    this.promise = new Promise((resolve, reject) => {
      this.reject = reject
      this.resolve = resolve
    })
  }
}

Then the mock is something like:

    const deferred = new Deferred()
    const spyOnFuncA = jest.spyOn(module2, 'funcA').mockImplementation(() => deferred.promise)
    mainFunc() // avoid uncaught exceptions with `.catch`
      .catch(err => expect(err).toBe(null))
    expect(spyOnFuncA).toBeCalled()
    expect(spyOnFuncB).not.toBeCalled()
    await deferred.resolve('whatever')
    expect(spyOnFuncB).toBeCalled()
Matt
  • 68,711
  • 7
  • 155
  • 158
  • Thanks alot this worked for me, I just needed to add `await` before the `deferred.resolve('whatever')` – tosh Sep 14 '22 at 10:58
0

The fake timer approach is correct, but you need to wait for the promise returned from mainFunc completed.

module1.js:

import * as module2 from './module2';

export async function mainFunc() {
  await module2.funcA();
  await module2.funcB();
}

module2.js:

export async function funcA() {
  // making some async operation
}

export async function funcB() {
  // making some async operation
}

module1.test.js:

import { mainFunc } from './module1';
import * as module2 from './module2';

describe('73707110', () => {
  test('Should not call second function until first function resolved', async () => {
    jest.useFakeTimers();

    const spyOnFuncA = jest.spyOn(module2, 'funcA').mockImplementation(() => new Promise((r) => setTimeout(r, 1000)));
    const spyOnFuncB = jest.spyOn(module2, 'funcB').mockImplementation();

    const promise = mainFunc();
    expect(spyOnFuncA).toBeCalled();
    expect(spyOnFuncB).not.toBeCalled();
    jest.runAllTimers();
    await promise;
    expect(spyOnFuncB).toBeCalled();
  });
});

Test result:

 PASS  stackoverflow/73707110/module1.test.ts (10.534 s)
  73707110
    ✓ Should not call second function until first function resolved (4 ms)

------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------|---------|----------|---------|---------|-------------------
All files   |     100 |      100 |   33.33 |     100 |                   
 module1.ts |     100 |      100 |     100 |     100 |                   
 module2.ts |     100 |      100 |       0 |     100 |                   
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        11.156 s

"jest": "^26.6.3"

Lin Du
  • 88,126
  • 95
  • 281
  • 483
  • Thank you for your comment. The test did pass, but I don't think that it's the right approach. Waiting for the main function to complete is not really aligned with what I want to test. Say I have 5 functions one after the other inside mainFunc, I still want the 2nd to run after the 1st one resolved, that doesn't mean that mainFunc was completed. – tosh Sep 14 '22 at 11:04