何时在 Rspec 的 expect 方法中使用花括号和括号?

29

我有一个测试做了这件事:

expect(@parser.parse('adsadasdas')).to raise_error(Errno::ENOENT)

但它没有起作用。我改成了:

expect { @parser.parse('adsadasdas') }.to raise_error(Errno::ENOENT)

它有效了。

我们在 expect 中何时使用花括号,何时使用圆括号?

4个回答

46
针对 OP 的评论,我已经编辑和完全重写了我的回答。我意识到我的原始回答过于简化,以至于可以被认为是不正确的。
实际上,这个StackOverflow问题已经在某种程度上回答了您的问题。
其中一位发帖者Peter Alfvin提出了一个很好的观点:
“关于规则,如果你想要测试行为(例如引发错误、改变某个值),那么你需要传递一个代码块或 Proc。否则,你应该传递一个“传统”的参数,然后测试该参数的值。”
你遇到的现象与引发错误有关。当您将@parser.parse('adsadasdas')作为参数(使用括号)传递给expect时,您实际上是在告诉 Ruby:
1.首先评估@parser.parse('adsadasdas')。 2.取得结果并将其传递给expect。 3.expect应查看此结果是否符合我的预期(也就是说,是否会引发Errno:ENOENT)。
但实际情况是:当Ruby评估@parser.parse('adsadasdas')时,错误会立即引发。Ruby甚至没有机会将结果传递给expect。(就我们所知,您可以将@parser.parse('adsadasdas')作为参数传递给任何函数……比如multiply()capitalize())错误已经被引发了,expect甚至没有机会完成它的工作。
但是,当您使用花括号将@parser.parse('adsadasdas')作为代码块(一个 Proc)传递给expect时,您要告诉 Ruby 的是:
1.expect,准备开始工作。 2.expect,我希望你在评估@parser.parse('adsadasdas')时跟踪发生的情况。 3.好的,expect,刚刚执行的代码块是否引发Errno:ENOENT错误?我希望它会这样做。
当您将代码块传递给expect时,您要告诉expect,您希望它检查代码块执行后产生的行为变化,并让您知道它是否符合您提供的预期。
当您向expect传递参数时,您正在告诉 ruby expect参与之前评估该参数以得出某个值,然后将该值传递给expect以查看是否符合某些期望。

谢谢你的回答。但是看看这里:https://github.com/rspec/rspec-expectations -- 它把expect作为一个带有括号的方法来使用...为什么呢? - Hommer Smith

11
TL;DR: 使用 expect(exp) 来指定关于 exp 的某些内容,使用 expect { exp } 来指定在执行 exp 时发生的 副作用
让我们详细解释一下。Rspec的大部分匹配器都是值匹配器,可以与任何Ruby对象进行匹配(或不匹配)。相比之下,少数Rspec的匹配器只能与块匹配,因为它们必须在块执行时观察块以正常运行。这些匹配器涉及发生的副作用(或未发生)的情况,而匹配器没有办法判断是否发生了命名的副作用,除非传递一个块来执行。让我们逐个考虑内置的块匹配器(截至Rspec 3.1):

raise_error

请注意,一个方法可以返回异常,这与引发异常不同。抛出异常是一种副作用,只有通过使用适当的rescue子句执行块,匹配器才能观察到它。因此,此匹配器必须接收一个块才能正常工作。

throw_symbol

抛出符号类似于引发错误——它会导致堆栈跳转,并且是一种只能通过在适当的catch块中运行块来观察的副作用。

change

Mutation to state 是一种副作用。匹配器只能通过检查先前的状态、运行块,然后检查之后的状态来判断某些状态是否发生了改变。
输出是一种副作用。为了使 output 匹配器正常工作,它必须使用新的 StringIO 替换适当的流($stdout 或 $stderr),执行块,将流恢复到其原始值,然后检查 StringIO 的内容。
yield_control/yield_with_args/yield_with_no_args/yield_with_successive_args 这些匹配器有所不同。 Yielding 不是真正的副作用(它实际上只是语法糖,用于调用调用方提供的另一个函数),但通过查看表达式的返回值无法观察到 yielding。为了使 yield 匹配器正常工作,它们提供了一个探针对象,您可以使用 &probe 语法将其传递给测试方法作为块:
expect { |probe| [1, 2, 3].each(&probe) }.to yield_with_successive_args(1, 2, 3)

所有这些匹配器有什么共同点?它们都不能用于简单的 Ruby 值。相反,它们都必须在适当的上下文中包装一个块(即在值之前/之后捕获或检查)。

请注意,在 RSpec 3 中,我们 添加了一些逻辑,以便在用户使用给定匹配器的错误 expect 形式时为其提供清晰的错误信息。然而,在特定情况下,即 expect(do_something).to raise_error,我们无法为您提供明确的解释 - 如果 do_something 引发错误(正如您所期望的那样...),则错误会在 Ruby 评估 to 参数(即 raise_error 匹配器)之前引发,因此 RSpec 无法检查匹配器是否支持值或块期望。


副作用指的是对某种状态的修改。 - notapatch

6

简而言之:

  • 使用花括号(代码块):当您想测试行为
  • 使用括号:当您想测试返回值

值得一读:至于规则,如果要测试行为(例如引发错误、更改某个值),则传递一个块或Proc。否则,传递一个“常规”参数,在这种情况下,该参数的值是被测试的。 - 来自这个回答


2
能够用简短、简洁的方式概括事情是一种天赋。 - fatfrog

1
在使用括号写的测试中,代码会被正常执行,包括所有的错误处理。花括号语法定义了一个块对象,在其中可以放置期望的代码。它封装了你期望出错的代码,并允许rspec捕获错误并提供自己的处理(在这种情况下是一个成功的测试)。
你也可以这样想:使用括号时,代码在传递给expect方法之前就被执行了,但使用块时,expect将自己运行代码。

谢谢你的回答。我仍然不明白何时需要在expect中使用{}或()。expect { @parser.parse(url) }.to include(known_url)不起作用,但如果我使用括号,它就可以工作。为什么? - Hommer Smith

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