异步函数的行为不像我对 Jest 的预期那样

Posted

技术标签:

【中文标题】异步函数的行为不像我对 Jest 的预期那样【英文标题】:Async function not behaving as I expect with Jest 【发布时间】:2018-04-26 19:21:48 【问题描述】:

javascript Jest 测试框架documentation 说我需要在我的回调中添加done() 来测试异步函数,否则测试完成后函数会返回,所以测试会失败。我已将 Jest 添加到我的 package.json 中,以及以下两个文件:

src.js:

function fetchData(cb) 
  setTimeout(cb, 2000, 'peanut butter')


module.exports = fetchData

src.test.js:

const fetchData = require('./src')

test('the data is peanut butter', () => 
  function callback(data) 
    expect(data).toBe('peanut butter')
    // no done() method!
  

  fetchData(callback);
)

我使用上面的代码通过了测试,但我认为我的测试应该失败,因为我的测试文件中没有done()。我的fetchData() 方法不是异步的吗?


编辑:按照 Nicholas 的回答,我将代码更改为:

src.js:

function fetchData(cb) 
  setTimeout(cb, 2000, 'peanut')


module.exports = fetchData

src.test.js:

const fetchData = require('./src')

test('the data is peanut butter', () => 
  function callback(data) 
    expect(data).toBe('peanut butter')
    done()
  

  fetchData(callback);
)

测试运行者应根据 Jest 文档评估期望/断言并失败(花生通过,花生酱预期),但仍显示通过测试。

此外,Jest 文档说:

If done() is never called, the test will fail, which is what you want to happen

测试在回调中有和没有 done() 方法的情况下,以及在有和没有传递给回调的正确 (peanut butter) 参数的情况下都通过(即所有四个变体都通过)。

【问题讨论】:

如果您不将done 作为参数传递,则代码被认为是同步的并且不会等待。 你的断言有机会运行之前,测试被认为是成功的 不管我设置了多长时间,测试运行器都会等待计时器完成,然后在我的控制台中报告测试结果。但是文档说我需要做一些特别的事情来处理异步代码——期望对象肯定是异步执行的(作为setTimeout()的回调) 测试运行器(或者更具体地说是 Node 本身)在终止进程之前等待最后注册的 setTimeout 回调到期。尝试创建一个空文件并在其中放入一个setTimeout。您会看到它在触发之前不会退出进程 Jest 有一个完全的陷阱,它修改了计时函数的行为。我已经编辑了我的答案以包含一个工作示例和对讨论该行为的问题的引用 同样,在您包含的编辑中,您没有将done 作为参数传递给testdone 也应该作为参数传递,除了在你完成测试时也会被调用。看看我的例子,运行它,你就会明白这一点。注意第 8 行,我将test 作为参数传递 【参考方案1】:

如果您不将done 作为参数传递,则测试运行程序会认为代码是同步的并且不会等待特殊情况发生——在这种情况下会调用done

因此,在您的断言/期望有机会运行之前,测试被标记为成功。

【讨论】:

如果你通过但没有调用done,测试应该会在一段时间后超时。在我使用的 Mocha 中,如果不调用 done,默认情况下会在 2000 毫秒后超时 覆盖这些原生函数在我的书中是一个很大的禁忌。他们本可以创建自己的计时功能,这样可以避免一半的问题和我们的时间。至于done is never called should fail the test,这仅适用于您实际上将if done 作为参数传递的情况,而您在任何示例中都没有这样做。希望有帮助 Jest 默认不会覆盖计时器,但它提供jest.useFakeTimers()模拟 手动(细粒度)控制的计时器。见Guides - Timer Mocks。 那么在我的示例中,setTimeout 是怎么来的 - 它直接在规范文件中声明,除非我用 useFakeTimers()/runAllTimers() 做我所做的事情,否则它不会打勾? @NicholasKyriakides 在您的示例中,expect(data).toBe('foo butter') 断言失败,这会抛出,因此无法到达 done() 行。正是因为 Jest 没有劫持 setTimeout(除非你使用 jest.uesFakeTimers),它不知道它抛出了。 #2980 中显示了一种解决方法。这个问题不会出现在promise中,因为如果断言在.then回调中抛出,promise会被拒绝并且它表示失败。【参考方案2】:

您的fetchData 本身是异步的,Jest 对此没有影响。另一方面,Jest 需要知道测试何时完成,通常是测试函数退出的时间,但这仅涵盖同步代码。在您的情况下,当您的测试函数退出时,您还没有调用任何断言,Jest 认为这是成功的(因为没有失败)。

为了证明没有调用任何断言,我将使用expect.assertions(number) 仅在它完全调用number 断言的情况下通过测试。例如,如果您在示例中将其设置为 1:

test('the data is peanut butter', () => 
  // Require exactly 1 assertion to pass the test
  expect.assertions(1)

  function callback(data) 
    expect(data).toBe('peanut butter')
  

  fetchData(callback)
)

你会看到错误信息:

● the data is peanut butter

  expect.assertions(1)

  Expected one assertion to be called but received zero assertion calls.

如您所见,没有调用任何断言。除了演示目的之外,您还可以使用expect.assertions 来确保在未调用断言时您的测试失败。

对于任何异步任务,您需要让 Jest 知道测试是异步的,并且您需要在测试完成时通知 Jest。一种方法是使用done 回调。当您的测试函数接受一个参数(通常称为done)时,Jest 会将其视为异步测试并等待直到您调用done() 回调,或达到超时阈值(默认值:5s)。在您编辑的示例中,您的函数采用零参数,并且您对 done() 的调用不是 Jest 的回调。

test('done callback', done => 
  //                  ^^^^ 1 argument
  // Jest waits until this callback is called or the timeout was reached.
  // ...
)

test('no callback argument', () => 
  //                         ^^ no argument
  // Test finishes as soon as the function exits
  // ...
)

这已经很少使用了,因为 Promise 现在更常见了,它们使这变得更简单,因为你可以返回 Promise,Jest 将等待它的完成而无需任何额外的设置。此外,您可以使用 asyncawait 使其更加美观,因为它是 Promise 之上的语法糖。

另见Testing Asynchronous Code - Promises和Testing Asynchronous Code - Async/Await

【讨论】:

这个答案并没有真正表明 OP 当前代码的问题是什么,而是深入研究了非常多余的信息。 它只是在谈论没有被调用的断言。但我对其进行了编辑以澄清expect.assertions 用于演示目的。

以上是关于异步函数的行为不像我对 Jest 的预期那样的主要内容,如果未能解决你的问题,请参考以下文章

Clickhouse:runningAccumulate() 不像我预期的那样工作

sleep() 不像我预期的那样工作

三.js:边界框奇怪的行为

每次测试都是Mock模块

IncludeExceptionDetailInFaults 的行为不像想象的那样

UWP ItemsControl 内容不像 WPF 中那样拉伸