Async beforeAll() does not finish before beforeEach() is called
Asked Answered
A

4

16

In Jest, beforeAll() is supposed to run before beforeEach().

The problem is that when I use an async callback for beforeAll(), Jest doesn't wait for the callback to finish before going on to beforeEach().

How can I force Jest to wait for an async beforeAll() callback to finish before proceeding to beforeEach()?

Minimal reproducible example

tests/myTest.test.js

const connectToMongo = require('../my_async_callback')    

// This uses an async callback.
beforeAll(connectToMongo) 

// This is entered before the beforeAll block finishes. =,(
beforeEach(() => { 
  console.log('entered body of beforeEach')  
})

test('t1'), () => {
  expect(1).toBe(1)
}
test('t2'), () => {
  expect(2+2).toBe(4)
}
test('t3'), () => {
  expect(3+3+3).toBe(9)
}

my_async_callback.js

const connectToMongo = async () => {
  try {
    await mongoose.connect(config.MONGODB_URI, { 
      useNewUrlParser: true, 
      useUnifiedTopology: true, 
      useFindAndModify: false, 
      useCreateIndex: true 
    })
    console.log('Connected to MongoDB')
  } catch (err) {
    console.log(`Error connecting to MongoDB: ${err.message}`)
  }
}

module.exports = connectToMongo

UPDATE: As the accepted answer helpfully points out, Jest actually does wait for beforeAll to finish first, except in the case of a broken Promise chain or a timeout. So, the premise of my question is false. My connectToMongo function was timing out, and simply increasing the Jest timeout solved the problem.

Amin answered 14/2, 2021 at 8:58 Comment(4)
This is expected behaviour of Jest. The names of beforeAll and beforeEach suggest only that they will be evaluated before all and each tests, the order of execution is not guaranteed. If you want In this case the problem is irrelevant because it's not Mongo but Mongoose. Mongoose chains connection promise internally, you can skip waiting for a connection. Notice that catch (err) is a mistake because it suppresses an error (though this won't prevent beforeAll from proceeding).Driveway
@EstusFlask "In this case the problem is irrelevant because it's not Mongo but Mongoose[...]" Yet, the DB requests time out when using separate beforeAll and beforeEach despite working otherwise. That is, they work when I connect in beforeEach (as ugly as that is) or omit Jest altogether. What could explain this behavior, if not Mongoose execution being order-dependent?Amin
@EstusFlask Also, could you please elaborate on "Notice that catch (err) is a mistake because it suppresses an error."? What would be the ideal way of catching this error, then?Amin
An error should be caught by a caller (Jest in this case), so there should be no catch, or at least rethrow an error. I'd expect it to work without async..await in beforeAll but I can't confirm this. Any way, for such order you may want custom wrapper over beforeEach, something like, let mongoProm; let beforeEachMongo = async (cb) => { mongoProm = mongoProm || mongoose.connection(...); await mongoProm; return cb() }.Driveway
N
15

The problem is that when I use an async callback for beforeAll(), Jest doesn't wait for the callback to finish before going on to beforeEach().

How can I force Jest to wait for an async beforeAll() callback to finish before proceeding to beforeEach()?

TLDR

The short answer is that Jest does wait for an async beforeAll() callback to finish before proceeding to beforeEach().

This means that if beforeEach() is running before something that should run in beforeAll() then the Promise chain must be broken or the beforeAll function is timing out.


Queue Runner in Jest

All of the beforeAll, beforeEach, test, afterEach, afterAll functions associated with a test are collected in queueableFns and are chained on these lines in queueRunner.ts:

  const result = options.queueableFns.reduce(
    (promise, fn) => promise.then(() => mapper(fn)),
    Promise.resolve(),
  );

So Jest starts with a resolved Promise and chains every function, in order, to the Promise chain with .then.


This behavior can be seen with the following test:

const order = [];

// first beforeAll with async function
beforeAll(async () => {
  order.push(1);
  await new Promise((resolve) => { setTimeout(resolve, 1000); });
  order.push(2);
});

// first beforeEach with done callback
beforeEach(done => {
  order.push(4);
  setTimeout(() => {
    order.push(6);
    done();
  }, 1000);
  order.push(5);
});

// second beforeEach
beforeEach(() => {
  order.push(7);
});

// second beforeAll
beforeAll(() => {
  order.push(3);
});

it("should run in order", () => {
  expect(order).toEqual([1, 2, 3, 4, 5, 6, 7]);  // SUCCESS!
});


Broken Promise Chain

If beforeEach is running before something that should run in beforeAll then it is possible the Promise chain is broken:

const order = [];

// does not return Promise and will break the Promise chain
const func = () => {
  setTimeout(() => { order.push(2); }, 1000);
}

const asyncFunc = async () => {
  order.push(1);
  await func();  // doesn't actually wait for 2 to be pushed
  order.push(3);
}

beforeAll(asyncFunc);

beforeEach(() => {
  order.push(4);
});

it("should run in order", () => {
  expect(order).toEqual([1, 2, 3, 4]);  // FAIL: [1, 3, 4]
});

Timeout

...or there is a timeout (note that the timeout will be reported by Jest in the output):

const order = [];

jest.setTimeout(100);  // 100ms timeout

const asyncFunc = async () => {
  order.push(1);
  await new Promise(resolve => { setTimeout(resolve, 1000); });  // times out
  order.push(2);
}

beforeAll(asyncFunc);

beforeEach(() => {
  order.push(3);
});

it("should run in order", () => {
  expect(order).toEqual([1, 2, 3]);  // FAIL: [1, 3] and Timeout error
});
Nola answered 23/2, 2021 at 21:9 Comment(1)
Excellent! It turns out my connectToMongo function was simply timing out since my connection was slow. Increasing the testTimeout property in Jest solved the problem.Amin
F
0

If there is async function and callback you can call done. if you want to pass callback inside the async function you are free!

Let me show you;

beforeAll(async (done) => {
  await connectToMongo().catch(done) // if there is error it finish with error payload
  done(); // it says i am finish. also you can use it on your callback function to say i am done.
})
Fridafriday answered 20/2, 2021 at 12:21 Comment(1)
Unfortunately, this doesn't work. I've tried both done and returning a Promise, as recommended in the Jest docs (jestjs.io/docs/en/asynchronous). Jest is still entering beforeEach before the connection in beforeAll is established.Amin
T
0

This happened to me when upgrading to angular 14. Solution was to upgrade zone.js to 0.11.8. Found solution here: https://github.com/angular/angular/issues/45476#issuecomment-1195153212

Tita answered 19/9, 2022 at 8:33 Comment(0)
C
-1

connectToMongo function is an async function, not a async callback (async function with a function as a parameter ???)

beforeEach will be called when beforeAll is finished, and it still works well.

beforeAll(connectToMongo) will be done right after you call it, this means, it will not wait until the db connect success.

Just wait until connectToMongo done and continue:

beforeAll(async () => { // async function
    await connectToMongo() // wait until connected to db
}) 
Cairn answered 14/2, 2021 at 12:39 Comment(2)
connectToMongo is a callback and is also async. Unless it uses params that can interefere with Jest's done (it doesn't), it's perfectly ok to use it as beforeAll(connectToMongo). will be done right after you call it - it won't. It would be if it were beforeAll(connectToMongo()), but it isn't, and this is not how callbacks are used.Driveway
As Estus says, this answer uses a wrong definition of callback as of this writing. (For the correct definition, see developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/…). It also doesn't solve my problem since connectToMongo is already async, so the provided code does nothing more than what I wrote.Amin

© 2022 - 2024 — McMap. All rights reserved.