在调用beforeEach()之前,Async beforeAll()没有完成

13

在 Jest 中,beforeAll() 应该在 beforeEach() 之前运行。

问题是,当我为 beforeAll() 使用异步回调时,Jest 在继续执行 beforeEach() 之前不等待回调完成。

如何强制 Jest 等待异步的 beforeAll() 回调完成后再执行 beforeEach()

最小可重现示例

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)
}

我的异步回调函数.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

更新:正如被接受的回答所指出的那样,Jest实际上会等待beforeAll先完成,除非存在损坏的Promise链或超时的情况。 因此,我的问题前提是错误的。 我的connectToMongo函数正在超时,简单地增加Jest的超时时间就解决了这个问题。


这是Jest的预期行为。beforeAll和beforeEach的名称仅表明它们将在所有测试之前和每个测试之前进行评估,但执行顺序不能保证。如果您想在这种情况下解决问题,则无关紧要,因为它不是Mongo而是Mongoose。Mongoose在内部链接连接承诺,因此您可以跳过等待连接。请注意,catch(err)是一个错误,因为它会抑制错误(尽管这不会阻止beforeAll继续进行)。 - Estus Flask
@EstusFlask:“在这种情况下,问题并不重要,因为问题不是来自Mongo而是Mongoose[...]”然而,使用单独的beforeAllbeforeEach时,DB请求超时,尽管其他方面工作正常。也就是说,在beforeEach中连接时(虽然很丑),或者完全省略Jest时,它们可以工作。如果不是Mongoose执行顺序相关,那么是什么原因导致了这种行为呢? - Asker
@EstusFlask,你能否详细说明一下为什么“注意 catch (err) 是一个错误,因为它抑制了一个错误。”?那么,捕获这个错误的最佳方式是什么? - Asker
1
一个错误应该被调用者(在这种情况下是Jest)捕获,所以不应该有catch,或者至少重新抛出一个错误。我期望它可以在beforeAll中不使用async..await工作,但我无法确认这一点。无论如何,对于这样的顺序,您可能希望在beforeEach上自定义包装器,例如:let mongoProm; let beforeEachMongo = async (cb) => { mongoProm = mongoProm || mongoose.connection(...); await mongoProm; return cb() } - Estus Flask
4个回答

12
问题在于当我为beforeAll()使用异步回调时,Jest在继续执行beforeEach()之前不等待回调完成。
如何强制Jest在继续执行beforeEach()之前等待异步的beforeAll()回调完成?
TLDR
简短的答案是,Jest确实会等待异步的beforeAll()回调完成后才会继续执行beforeEach()
这意味着如果beforeEach()beforeAll()应该运行的东西之前运行,则Promise链必须被打破,或者beforeAll函数超时了。

Jest中的队列运行器

与测试相关的所有beforeAllbeforeEachtestafterEachafterAll函数都被收集在queueableFns中,并在queueRunner.ts中链接在一起这些行上

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

Jest从一个已解决的Promise开始,并按顺序使用.then将每个函数链接到Promise链中。


以下测试可以展示这种行为:

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!
});


破碎的Promise链

如果beforeEach在应该在beforeAll中运行的某个操作之前运行,则可能会导致Promise链断裂:

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]
});

超时

......或者发生了超时(请注意,Jest将在输出中报告超时):

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
});

1
太好了!原来我的 connectToMongo 函数只是因为连接速度慢而超时了。在 Jest 中增加 testTimeout 属性解决了这个问题。 - Asker

0
如果有异步函数和回调,你可以调用 done。如果你想在异步函数内部传递回调,那么你是自由的!
让我给你展示一下;
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.
})

2
不幸的是,这并不起作用。我已经尝试过使用done和返回Promise,正如Jest文档中推荐的那样(https://jestjs.io/docs/en/asynchronous)。Jest仍然在建立`beforeAll`中的连接之前进入`beforeEach`。 - Asker

0

-1

connectToMongo 函数是一个异步函数,不是一个 async callback(将函数作为参数的异步函数?)

beforeEach 会在 beforeAll 完成后调用,并且它依然能正常工作。

beforeAll(connectToMongo) 将会在你调用它后立即执行,这意味着,它不会等待数据库连接成功。

只需等待 connectToMongo 完成并继续执行:

beforeAll(async () => { // async function
    await connectToMongo() // wait until connected to db
}) 

1
connectToMongo是一个回调函数,也是async。除非它使用的参数会干扰Jest的done(它不会),否则将其用作beforeAll(connectToMongo)是完全可以的。它不会在你调用它后立即完成 - 如果它是beforeAll(connectToMongo()),那么它就会了,但它并不是这样使用回调函数的方式。 - Estus Flask
正如Estus所说,截至本篇回答撰写时,该回答使用了错误的"callback"定义。(如果需要正确的定义,请参见https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing#async_callbacks)。此外,由于 'connectToMongo' 已经是异步的,所以提供的代码与我编写的代码没有任何区别,无法解决我的问题。 - Asker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接