RSpec 3.2 已发布!

Myron Marston

2015 年 2 月 3 日

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

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

感谢所有帮助实现此版本的人!

值得注意的更改

Windows CI

RSpec 一直支持 Windows,但所有核心维护人员都在 POSIX 系统上开发的事实偶尔会使这变得困难。当 RSpec 3.1 发布时,我们不幸地破坏了 Windows 上的几件事,直到一些用户报告了这些问题(后来在 3.1.x 补丁版本中修复)我们才得知。我们希望防止这种情况再次发生,因此这次我们努力让 Windows CI 构建在 AppVeyor 上运行。

我们的 Windows 构建已经通过,但这里还有更多工作要做。如果您是 RSpec 和 Windows 用户,并且想帮助我们,请联系我们!

核心:待定示例输出包含失败细节

RSpec 3.0 附带了 pending 的新语义。与其跳过示例(现在可以使用 skip 来实现),待定示例会执行,并期望出现失败,如果沒有失败,它们会失败以通知您,这些示例不再需要标记为 pending。这种更改非常有用,因为它确保每个 pending 示例都需要是待定的。但是,格式化程序输出并没有指示导致示例保持待定的失败,因此您必须取消待定示例才能看到该详细信息。

在 RSpec 3.2 中,我们纠正了这一点:pending 示例输出现在包含执行示例时发生的失败。

核心:每个示例现在都有一个单例组

RSpec 有许多基于元数据的特性,到目前为止,这些特性只适用于示例组,而不适用于单个示例。

RSpec.configure do |config|
  # 1. Filtered module inclusion: examples in groups tagged with `:uses_time`
  #    will have access to the instance methods of `TimeHelpers`
  config.include TimeHelpers, :uses_time

  # 2. Context hooks: a browser will start before groups tagged
  #    with `:uses_browser` and shutdown afterwards.
  config.before(:context, :uses_browser) { Browser.start }
  config.after( :context, :uses_browser) { Browser.shutdown }
end

# 3. Shared context auto-inclusion: groups tagged with
#    `:uses_redis` will have this context included.
RSpec.shared_context "Uses Redis", :uses_redis do
  let(:redis) { Redis.connect(ENV['REDIS_URL']) }
  before { redis.flushdb }
end

在每种情况下,用适当元数据标记的单个示例都不会应用这些构造,这很容易忘记。这在 RSpec 3.2 中正在改变:我们现在将每个示例视为隐式地属于仅包含一个示例的单例示例组的一部分,这使我们能够将这些构造应用于单个示例,而不仅仅是组。如果您熟悉 Ruby 的对象模型,这可能听起来很熟悉——Ruby 中的每个对象都有一个单例类,它包含只适用于该实例的自定义行为。此特性是在 Ruby 的单例类之上简单实现的。

核心:性能改进

我们在优化 RSpec 3.2 的 rspec-core 性能方面投入了大量精力。除了某些算法更改和许多微优化之外,我们还找到了大幅减少 rspec-core 完成的对象分配数量的方法。根据我的 基准测试,RSpec 3.2 分配的对象比 RSpec 3.1 少约 30%,同时获得了许多新特性。

在未来的版本中,我希望我们能将类似的改进应用于 rspec-expectations 和 rspec-mocks。

核心:新的沙箱 API

RSpec 使用 RSpec 进行测试。在 rspec-core 中,我们规范套件中的许多规范定义并运行示例组和示例,以验证当在真实的 RSpec 上下文中运行时,RSpec 的各种构造如何关联的行为。为了促进这种自食其力的行为,rspec-core 的规范套件长期以来一直使用沙箱技术。每个规范都可以自由地改变配置并定义示例组和示例,而无需担心这会如何干扰 RSpec 运行程序的内部状态。

这种沙箱显然对 RSpec 自己的内部使用很有用,但它也对测试第三方 RSpec 扩展很有用。我们现在将其作为新的公共 API 提供。

# You have to require this file to make this API available...
require 'rspec/core/sandbox'

RSpec.configure do |config|
  config.around do |ex|
    RSpec::Core::Sandbox.sandboxed(&ex)
  end
end

感谢 Tyler Ball 实现了这一点

核心:共享示例组改进

在此版本中,我们修复了与共享示例组相关的几个长期存在的错误和 annoyances。

如果您过去遇到过由于这些问题导致的共享示例组问题,您可能想再次尝试一下。

期望:用于 DSL 定义的自定义匹配器的链式简写

自定义匹配器 DSL 具有 chain 方法,该方法使向您的匹配器添加流畅接口变得容易。

RSpec::Matchers.define :be_bigger_than do |min|
  chain :but_smaller_than do |max|
    @max = max
  end

  match do |value|
    value > min && value < @max
  end
end

# usage:
expect(10).to be_bigger_than(5).but_smaller_than(15)

如本例所示,chain 的最常见用法是接受一个额外的参数,该参数在 match 逻辑中以某种方式使用。Tom Stuart 建议并实现 了一个改进,允许您将匹配器定义缩短为

RSpec::Matchers.define :be_bigger_than do |min|
  chain :but_smaller_than, :max
  match do |value|
    value > min && value < max
  end
end

chain 的第二个参数——:max——用于定义一个 max 属性,该属性设置为传递给 but_smaller_than 的参数。

感谢,Tom Stuart!

期望:输出匹配器可以处理子进程

RSpec 3.0 附带了一个新的 output 匹配器,允许您指定预期输出到 stdoutstderr

expect { print 'foo' }.to output('foo').to_stdout
expect { warn  'foo' }.to output(/foo/).to_stderr

用于这些匹配器的机制——暂时用 StringIO 替换 $stdout$stderr,持续时间为块——非常简单,但当您生成将输出到这些流之一的子进程时,它不起作用。例如,这会失败

expect { system('echo foo') }.to output("foo\n").to_stdout

在 RSpec 3.2 中,您可以用 to_stdout_from_any_process 替换 to_stdout,并且此期望将起作用。

expect { system('echo foo') }.to output("foo\n").to_stdout_from_any_process

这使用了一种备用机制,其中 $stdout 暂时重新打开到临时文件,该机制可以与子进程一起使用。不幸的是,它也慢得多——在我们的 基准测试 中,它慢了 30 倍!因此,您必须使用 to_std(out|err)_from_any_process 而不是 to_std(out|err) 来选择它。

感谢 Alex Genco 实现了 此改进!

期望:DSL 定义的自定义匹配器现在可以接收块

当使用 DSL 定义自定义匹配器时,在某些情况下,接收块会很不错

RSpec::Matchers.define :be_sorted_by do |&blk|
  match do |array|
    array.each_cons(2).all? do |a, b|
      (blk.call(a) <=> blk.call(b)) <= 0
    end
  end
end

# intended usage:
expect(users).to be_sorted_by(&:email)

不幸的是,Ruby 的限制不允许我们支持这一点。define 块使用 class_exec 执行,这确保它在新的匹配器类的上下文中评估,同时还允许我们将来自调用站点的参数转发到 define 块。传递给 class_exec 的块是要评估的块(在本例中,是 define 块),并且无法将两个块传递给 class_exec

在 RSpec 的早期版本中,传递给 be_sorted_by 的块会被静默忽略。在 RSpec 3.2 中,我们现在会发出一个警告

WARNING: Your `be_sorted_by` custom matcher receives a block argument (`blk`), but
due to limitations in ruby, RSpec cannot provide the block. Instead, use the
`block_arg` method to access the block. Called from path/to/file.rb:line_number.

…它告诉您另一种实现此目的的方法:新的 block_arg 方法,可以在自定义匹配器中使用。

RSpec::Matchers.define :be_sorted_by do
  match do |array|
    array.each_cons(2).all? do |a, b|
      (block_arg.call(a) <=> block_arg.call(b)) <= 0
    end
  end
end

感谢 Mike Dalton 实现了 此改进。

模拟:any_args 作为参数 splat 运作

RSpec 很久以前就有了 any_args 匹配器

expect(test_double).to receive(:message).with(any_args)

any_args 匹配器与它的字面意思完全相同:它允许任何参数(包括无参数)与消息期望匹配。在 RSpec 3.1 及之前的版本中,any_args 只能用作 with 的独立参数。在 RSpec 3.2 中,我们将其视为参数 splat,因此您现在可以在参数列表中的任何位置使用它

expect(test_double).to receive(:message).with(1, 2, any_args)

这将匹配诸如 test_double.message(1, 2)test_double.message(1, 2, 3, 4) 之类的调用。

模拟:不匹配的参数现在会被 diff

使 RSpec 的失败输出如此有用的一个重要部分是您从特定匹配器获得的失败的 diff。但是,消息期望失败从未包含 diff。例如,考虑这个失败的消息期望

test_double = double
expect(test_double).to receive(:foo).with(%w[ 1 2 3 4 ].join("\n"))
test_double.foo(%w[ 1 2 5 4 ].join("\n"))

在 RSpec 3.1 及之前的版本中,这会失败,并显示

Failure/Error: test_double.foo(%w[ 1 2 5 4 ].join("\n"))
  Double received :foo with unexpected arguments
    expected: ("1\n2\n3\n4")
         got: ("1\n2\n5\n4")

RSpec 3.2 现在在适当的时候(通常是在多行字符串中或当对象的漂亮打印输出是多行时)在消息期望失败中包含 diff。在 3.2 中,这会失败,并显示

Failure/Error: test_double.foo(%w[ 1 2 5 4 ].join("\n"))
  Double received :foo with unexpected arguments
    expected: ("1\n2\n3\n4")
         got: ("1\n2\n5\n4")
  Diff:
  @@ -1,5 +1,5 @@
   1
   2
  -3
  +5
   4

感谢 Pete Higgins 最初建议 此特性,并 从 rspec-expectation 中提取我们的 differ 到 rspec-support,并感谢 Sam Phippen 更新 rspec-mocks 以使用新提供的 differ。

模拟:验证双重可以被命名

RSpec 的 double 一直支持可选名称,该名称在消息期望失败中使用。在 RSpec 3.0 中,我们添加了一些新的测试双重类型——instance_doubleclass_doubleobject_double——在 3.1 中,我们添加了 instance_spyclass_spyobject_spy…但我们忘记了为这些类型支持可选名称。在 RSpec 3.2 中,这些双重类型现在支持可选名称。只需传递第二个参数(在接口参数之后,但在任何存根之前)

book_1 = instance_double(Book, "The Brothers Karamozov", author: "Fyodor Dostoyevsky")
book_2 = instance_double(Book, "Lord of the Rings", author: "J.R.R. Tolkien")

感谢 Cezary Baginski 实现了 此功能。

Rails:实例双重支持由 ActiveRecord 定义的动态列方法

ActiveRecord 基于底层表的列模式,在您的模型类上定义了许多方法。当您第一次调用这些方法中的一个时,就会动态地发生这种情况。不幸的是,这导致在使用基于 ActiveRecord 的 instance_double 时出现令人困惑的行为:由于 User.method_defined?(:email) 在第一次调用列方法之前返回 false,因此 instance_double(User) 最初不允许 email 被存根,直到列方法被动态定义。

我们 记录了此问题,但它仍然是用户经常感到困惑的来源。在 RSpec 3.2 中,我们已经解决了这个问题——我们现在强制模型在创建基于 ActiveRecord 的 instance_double 时定义列方法,以便验证后的双重按预期工作。

感谢 Jon Rowe 实现了 此改进!

Rails:支持 Ruby 2.2 与 Rails 3.2 和 4.x

Ruby 2.2 于 12 月发布,虽然大多数 Ruby 2.1 代码库在 2.2 上运行良好,但 Rails 核心团队不得不对 Rails 进行一些更改以支持 2.2。同样,Aaron Kromer 已经更新了 rspec-rails 以在 Rails 3.2Rails 4.x 上支持 Ruby 2.2。

Rails:ActionMailer 预览的新生成器

ActionMailer 预览是 Rails 4.1 中的 新功能 之一。默认情况下,Rails 在您的 test 目录中生成预览类。由于 RSpec 项目使用 spec 而不是 test,因此这与 rspec-rails 并不很好地集成。

在 3.2 中,rspec-rails 现在附带了一个生成器,它会将 ActionMailer 预览类放在您的 spec 目录中,提供与 Rails 已经为 Test::Unit 和 Minitest 用户提供的相同功能。

感谢 Takashi Nakagawa 提供了 初始实现,并感谢 Aaron Kromer 对其进行了 进一步改进

统计

合并

rspec-core

rspec-expectations

rspec-mocks

rspec-rails

rspec-support

文档

API 文档

Cucumber 功能

发布说明

rspec-core-3.2.0

完整变更日志

增强功能

错误修复

rspec-expectations-3.2.0

完整变更日志

增强功能

错误修复

rspec-mocks-3.2.0

完整变更日志

增强功能

错误修复

rspec-rails-3.2.0

完整变更日志

增强功能

错误修复

rspec-support-3.2.0

完整变更日志

增强功能

错误修复