RSpec 3.3 已发布!

Myron Marston

2015年6月12日

RSpec 3.3 刚刚发布!鉴于我们对语义版本控制的承诺,对于已经使用 RSpec 3.0、3.1 或 3.2 的任何人来说,这应该是一个简单的升级,但如果我们引入了任何回归,请告诉我们,我们会尽快发布一个补丁版本来修复。

RSpec 仍然是一个社区驱动的项目,来自世界各地的贡献者参与其中。此版本包含来自近 50 位不同贡献者的 769 次提交和 200 个合并的拉取请求!

感谢所有帮助完成此版本的人!

主要变更

核心:每个示例和示例组的唯一 ID

历史上,RSpec 示例主要通过文件位置进行标识。例如,以下命令

$ rspec spec/unit/baseball_spec.rb:23

…将运行在 spec/unit/baseball_spec.rb 文件的第 23 行定义的示例或示例组。基于位置的标识通常效果很好,但并不总是能唯一地标识特定示例。例如,如果您使用共享示例,您的规范套件可能在 spec/support/shared_examples.rb:47 定义了示例的多个副本。

RSpec 3.3 引入了一种新的方法来标识示例和示例组:唯一 ID。ID 针对特定文件进行范围限定,并基于示例或组的索引。例如,以下命令

$ rspec spec/unit/baseball_spec.rb[1:2,1:4]

…将运行在 spec/unit/baseball_spec.rb 文件中定义的第一个顶层组下的第 2 个和第 4 个示例或组。

在大多数情况下,新的示例 ID 主要在 RSpec 内部使用,以支持一些新的 3.3 功能,但您可以从命令行使用它们。RSpec 为每个失败打印的重新运行命令将使用它们(如果文件位置不能唯一地识别失败的示例)。现在,复制粘贴特定失败示例的重新运行命令将始终只运行该示例!

核心:新的 --only-failures 选项

现在 RSpec 拥有了一种强大的方法来唯一地识别每个示例,我们添加了新的过滤功能,允许您只运行失败的示例。要启用此功能,您首先需要配置 RSpec,使其知道在哪里持久化每个示例的状态

RSpec.configure do |c|
  c.example_status_persistence_file_path = "./spec/examples.txt"
end

配置完成后,RSpec 将在每次运行套件后开始持久化每个示例的状态。您可能需要将此文件添加到 .gitignore(或您的源代码控制系统的等效项),因为它不应被纳入源代码控制。配置完成后,您可以使用新的 CLI 选项

$ rspec --only-failures
# or apply it to a specific file or directory:
$ rspec spec/unit --only-failures

值得注意的是,此选项仅过滤上次运行失败的示例,而不是仅过滤上次运行 rspec 的失败。这意味着,例如,如果有一个您通常不会在本地运行的缓慢的验收规范(将其留给您的 CI 服务器运行),并且它上次在本地运行时失败,即使是在几周前,当您运行 rspec --only-failures 时,它也会被包含在内。

请参阅我们的relish 文档,了解此功能的端到端示例。

核心:新的 --next-failure 选项

当进行导致规范套件出现许多错误的更改时(例如重命名一个常用的方法),我经常使用一个特定的工作流程

  1. 运行整个套件以获取错误列表。
  2. 使用 RSpec 打印的重新运行命令,依次运行每个错误,在继续下一个错误之前修复每个示例。

这允许我系统地处理所有错误,而不必反复运行整个套件。RSpec 3.3 包含一个新选项,可以大大简化此工作流程

$ rspec --next-failure
# or apply it to a specific file or directory:
$ rspec spec/unit --next-failure

此选项等效于 --only-failures --fail-fast --order defined。它只过滤错误,并在第一个错误发生时中止。它应用 --order defined 以确保您在没有修复示例的情况下多次运行它时,始终获得相同的失败示例。

核心:稳定的随机排序

RSpec 的随机排序一直允许您传递特定的 --seed 以按与先前运行相同的顺序运行套件。但是,这仅在您运行与原始运行相同的示例集时才有效。如果您将种子应用于示例的子集,它们的排序相对于彼此不一定是一致的。这是 Array#shuffle 工作方式的结果:%w[ a b c d ].shuffle 可能将 c 排列在 b 之前,但即使您使用相同的随机种子,%w[ b c d ].shuffle 仍可能将 c 排列在 b 之后

这可能看起来并不重要,但它使 --seed 的用处大打折扣。当您尝试追踪排序依赖关系的来源时,您必须不断运行整个套件才能重现错误。

RSpec 3.3 解决了这个问题。我们不再使用 shuffle 进行随机排序。相反,我们将 --seed 值与每个示例的 id 组合在一起,对其进行哈希运算,并按生成的数值进行排序。这确保了,如果特定种子将示例 c 排列在示例 b 之前,无论您运行的是套件的哪个子集,当您重新使用该种子时,c 始终在 b 之前。

虽然稳定的随机排序本身很有用,但真正重要的在于它启用的一个新功能:二分查找。

核心:二分查找

自 RSpec 2.8 以来,RSpec 一直支持随机排序(使用 --seed 选项来重现特定排序)。这些功能有助于发现规范之间的排序依赖关系,您需要快速隔离并修复这些依赖关系。

不幸的是,RSpec 提供的隔离排序依赖关系的帮助很少。在 RSpec 3.3 中,情况正在发生变化。我们现在提供了一个 --bisect 选项,它可以将排序依赖关系缩小到最小的重现情况。新的二分查找标志会重复运行套件的子集,以隔离在运行整个套件时重现相同错误的最小示例集。

稳定的随机排序使您能够运行套件的各种子集,以尝试将排序依赖关系缩小到最小的重现情况。

请参阅我们的relish 文档,了解此功能的端到端示例。

核心:线程安全的 letsubject

历史上,letsubject 从不线程安全。这在 RSpec 3.3 中发生了改变,这要归功于 Josh Cheek 的出色工作

请注意,线程安全同步会增加一些开销,正如您所期望的那样。如果您在示例中没有启动任何线程,并且希望避免这种开销,您可以配置 RSpec 禁用线程安全性。

预期:新的 aggregate_failures API

当您需要对特定结果进行多个独立的预期时,通常有两个方法可以选择。一种方法是为每个预期定义一个单独的示例

RSpec.describe Client do
  let(:response) { Client.make_request }

  it "returns a 200 response" do
    expect(response.status).to eq(200)
  end

  it "indicates the response body is JSON" do
    expect(response.headers).to include("Content-Type" => "application/json")
  end

  it "returns a success message" do
    expect(response.body).to eq('{"message":"Success"}')
  end
end

这遵循“每个示例一个预期”的准则,鼓励您让每个规范专注于行为的一个方面。或者,您可以将所有预期放在一个示例中

RSpec.describe Client do
  it "returns a successful JSON response" do
    response = Client.make_request

    expect(response.status).to eq(200)
    expect(response.headers).to include("Content-Type" => "application/json")
    expect(response.body).to eq('{"message":"Success"}')
  end
end

后一种方法将更快,因为请求只执行一次,而不是三次。但是,如果状态码不是 200,则示例将在第一个预期处中止,您将无法看到后面两个预期是否通过。

RSpec 3.3 具有一个新功能,可以帮助您在出于速度或其他原因需要将多个预期放在一个示例中时。只需将预期包含在一个 aggregate_failures 块中

RSpec.describe Client do
  it "returns a successful JSON response" do
    response = Client.make_request

    aggregate_failures "testing response" do
      expect(response.status).to eq(200)
      expect(response.headers).to include("Content-Type" => "application/json")
      expect(response.body).to eq('{"message":"Success"}')
    end
  end
end

aggregate_failures 块中,预期错误不会导致示例中止。相反,将在最后引发一个单一的聚合异常,其中包含多个子错误,RSpec 会为您很好地格式化它们

1) Client returns a successful response
   Got 3 failures from failure aggregation block "testing reponse".
   # ./spec/client_spec.rb:5

   1.1) Failure/Error: expect(response.status).to eq(200)

          expected: 200
               got: 404

          (compared using ==)
        # ./spec/client_spec.rb:6

   1.2) Failure/Error: expect(response.headers).to include("Content-Type" => "application/json")
          expected {"Content-Type" => "text/plain"} to include {"Content-Type" => "application/json"}
          Diff:
          @@ -1,2 +1,2 @@
          -[{"Content-Type"=>"application/json"}]
          +"Content-Type" => "text/plain",
        # ./spec/client_spec.rb:7

   1.3) Failure/Error: expect(response.body).to eq('{"message":"Success"}')

          expected: "{\"message\":\"Success\"}"
               got: "Not Found"

          (compared using ==)
        # ./spec/client_spec.rb:8

RSpec::Core 通过使用元数据为此功能提供了改进的支持。无需将预期与 aggregate_failures 结合在一起,只需使用 :aggregate_failures 标记示例或组

RSpec.describe Client, :aggregate_failures do
  it "returns a successful JSON response" do
    response = Client.make_request

    expect(response.status).to eq(200)
    expect(response.headers).to include("Content-Type" => "application/json")
    expect(response.body).to eq('{"message":"Success"}')
  end
end

如果您想在所有地方启用此功能,可以使用 define_derived_metadata

RSpec.configure do |c|
  c.define_derived_metadata do |meta|
    meta[:aggregate_failures] = true unless meta.key?(:aggregate_failures)
  end
end

当然,您可能不希望在所有地方默认启用此功能。unless meta.key?(:aggregate_failures) 部分允许您通过使用 aggregate_failures: false 标记它们来选择退出单个示例或组。当您有依赖预期(例如,某个预期只有在先前预期通过时才有意义),或者如果您使用预期来表达先决条件时,您可能希望示例在预期失败时立即中止。

预期:改进的错误输出

RSpec 3.3 包含所有匹配器的错误消息的全面改进。测试双重现在在我们的错误消息中看起来更漂亮

Failure/Error: expect([foo]).to include(bar)
  expected [#<Double "Foo">] to include #<Double "Bar">
  Diff:
  @@ -1,2 +1,2 @@
  -[#<Double "Bar">]
  +[#<Double "Foo">]

此外,RSpec 对 Time 和其他对象的改进格式现在将用于检查这些对象的位置,无论您使用了哪个内置匹配器。因此,例如,您以前会得到

Failure/Error: expect([Time.now]).to include(Time.now)
  expected [2015-06-09 07:48:06 -0700] to include 2015-06-09 07:48:06 -0700

…现在您将得到

Failure/Error: expect([Time.now]).to include(Time.now)
  expected [2015-06-09 07:49:16.610635000 -0700] to include 2015-06-09 07:49:16.610644000 -0700
  Diff:
  @@ -1,2 +1,2 @@
  -[2015-06-09 07:49:16.610644000 -0700]
  +[2015-06-09 07:49:16.610635000 -0700]

…这使得时间对象在亚秒级别上的差异更加清晰。

感谢 Gavin Miller、Nicholas Chmielewski 和 Siva Gollapalli 对这些改进的贡献!

模拟:改进的错误输出

RSpec::Mocks 也对其错误输出进行了一些改进。RSpec 对 Time 和其他对象的改进格式现在也应用于模拟预期错误

Failure/Error: dbl.foo(Time.now)
  #<Double (anonymous)> received :foo with unexpected arguments
    expected: (2015-06-09 08:33:36.865827000 -0700)
         got: (2015-06-09 08:33:36.874197000 -0700)
  Diff:
  @@ -1,2 +1,2 @@
  -[2015-06-09 08:33:36.865827000 -0700]
  +[2015-06-09 08:33:36.874197000 -0700]

此外,have_received 的错误输出已得到很大改进,因此当预期参数不匹配时,它会列出每组实际参数,以及使用这些参数接收消息的次数

Failure/Error: expect(dbl).to have_received(:foo).with(3)
  #<Double (anonymous)> received :foo with unexpected arguments
    expected: (3)
         got: (1) (2 times)
              (2) (1 time)

感谢 John Ceh 实施了后一项改进!

模拟:模拟 MyClass.new 验证 MyClass#initialize

RSpec 的验证双重使用 Ruby 反射功能提供的元数据来验证,除其他事项外,传递的参数是否根据原始方法的签名有效。但是,当使用仅一个参数 splat 定义方法时

def method_1(*args)
  method_2(*args)
end

def method_2(a, b)
end

…那么验证双重将允许任何参数,即使该方法只是委派给另一个具有严格签名的方法。不幸的是,Class#new 是其中一个方法。它由 Ruby 定义为委派给 #initialize,并且只接受 initialize 签名可以处理的参数,但 MyClass.method(:new).parameters 提供的元数据表明它可以处理任何参数,即使它无法处理。

在 RSpec 3.3 中,我们改进了验证双重,因此当您在类上模拟 new 时,它使用 #initialize 的方法签名来验证参数,除非您重新定义了 new 以执行其他操作。这使验证双重能够在您向模拟的 MyClass#new 方法传递真实类不允许的参数时,为您提供错误。

Rails:生成的脚手架路由规范现在包含 PATCH 规范

Rails 4 添加了对 PATCH 的支持,将其作为更新的主要 HTTP 方法。rspec-rails 中的路由匹配器从 2.14 版本开始也支持 PATCH,但生成的脚手架规范没有更新以匹配。这个问题已在 rspec-rails 3.3 中得到 解决

感谢 Igor Zubkov 的改进!

Rails:新的 :job 规范类型

现在 ActiveJob 已成为 Rails 的一部分,rspec-rails 拥有新的 :job 规范类型,您可以通过以下两种方式选择使用它:在您的示例组中添加标签 :type => :job,或者将规范文件放在 spec/jobs 目录下(如果您启用了 infer_spec_type_from_file_location!)。

感谢 Gabe Martin-Dempesy 的改进!

统计数据

综合

rspec-core

rspec-expectations

rspec-mocks

rspec-rails

rspec-support

文档

API 文档

Cucumber 特性

发行说明

rspec-core-3.3.0

完整变更日志

增强功能

错误修复

rspec-expectations-3.3.0

完整变更日志

增强功能

错误修复

rspec-mocks-3.3.0

完整变更日志

增强功能

错误修复

rspec-rails-3.3.0

完整变更日志

增强功能

rspec-support-3.3.0

完整变更日志

增强功能

错误修复