最近 RSpec 配置警告和错误
Myron Marston
2011 年 11 月 4 日RSpec 2.6 在定义示例组后使用 RSpec.configure { ... }
时引入了弃用警告。在 RSpec 2.7 中,此警告已删除,现在在定义示例组后设置特定配置设置 (expect_with
和 mock_with
) 时将引发错误。
最近,一些人对这些更改提出了意见和抱怨,例如来自 几个 不同 人 的 推特。
我正是做出这些 RSpec 更改的人,人们对这些更改感到恼火是合情合理的…但这背后还有很多故事。我不确定解决我试图通过这些更改解决的问题的正确方案是什么,但我希望通过写博客来讨论它,我们可以从社区中获得一些好主意。
RSpec 2.0
RSpec 2 的主要目标之一是将规范运行器 (rspec-core) 与模拟框架 (rspec-mocks) 和期望框架 (rspec-expectations) 解耦。除了解耦是件好事™之外,这种分离还为人们提供了选择使用 RSpec 的哪些部分的新可能性。
特别是,它允许人们使用 rspec-core 使用 RSpec 的示例定义 DSL (describe
、it
、before
、let
等) 来定义和运行测试,同时坚持使用 Test::Unit 或 minitest 的 assert_foo
断言方法,而不是使用 RSpec 的 object.should whatever
语法。无论好坏,有些人真的很不喜欢 rspec-expectations,但喜欢这个运行器。
RSpec 2 允许您配置要使用哪个
# spec_helper.rb
RSpec.configure do |config|
config.expect_with :stdlib
end
# or
RSpec.configure do |config|
# not strictly necessary; this is the default config anyway
config.expect_with :rspec
end
rspec-expectations 和标准库断言都作为模块提供——分别是 RSpec::Matchers
和 Test::Unit::Assertions
。RSpec 2.0 到 2.5 在运行示例之前 (也就是说,在所有示例都定义之后) 将适当的模块包含到 RSpec::Core::ExampleGroup
中,以便人们可以根据需要进行配置。
不幸的是,这在 ruby 1.9 中触发了一个不幸的错误…我将在下面谈到它。
无限递归问题
在 RSpec 2 发布后不久,我们开始收到一些关于用户偶尔从 RSpec 获取 SystemStackError
的 间歇性 报告,表明正在发生无限递归。我自己在某一时刻在处理 rspec-core 规范时也遇到了这个错误。
递归总是在 rspec-expectations 的 method_missing 钩子 中发生。特别是,对 super
的调用触发了无限递归。
我对此感到非常、非常困惑,并花了几个小时进行故障排除,试图找出为什么 super
会无限递归自身。
这是 ruby 1.9 中的一个错误
我最终设法将问题简化为一个简单的无 rspec 示例
# example.rb
module MyModule
def some_method; super; end
end
class MyBaseClass; end
class MySubClass < MyBaseClass;
include MyModule
end
# To trigger this bug, we must include the module in the base class after
# the module has already been included in the subclass. If we move this line
# above the subclass declaration, this bug will not occur.
MyBaseClass.send(:include, MyModule)
MySubClass.new.some_method
如果您在 ruby 1.8 上运行此代码,您将 (正确地) 获得 NoMethodError
。在 ruby 1.9 上,您将获得无限递归和 SystemStackError
。以下是触发此错误的条件的简短说明
- 定义一个包含使用
super
的方法的模块。 - 在该模块已经被包含到它的一个子类之后,将该模块包含到一个类中。
- 创建一个所述子类的实例并调用该方法。
请注意,如果该模块是在子类之前包含在超类中,则不会发生此错误。
此错误如何在 RSpec 中表现出来
以下是此错误如何在 RSpec 中表现出来的
- 每次定义示例组 (使用
describe
或context
) 时,RSpec 都会创建一个RSpec::Core::ExampleGroup
的新子类,或者如果将示例组嵌套在另一个示例组中,则创建一个子类的子类。 - 一些用户将
RSpec::Matchers
包含到他们的示例组中。 这是一个例子. - 正如我上面所解释的,在 2.0 到 2.5 中,RSpec 在运行示例之前,在所有示例都定义之后 (因此,在用户可能已将
RSpec::Matchers
包含到他们的示例组之后) 将RSpec::Matchers
包含到RSpec::Core::ExampleGroup
中。 - 当用户从示例中调用未定义或拼写错误的方法时,它会触发此错误。
修复问题…引入其他问题 :(
为了防止用户获得无限递归,我们需要防止 RSpec::Matchers
(以及实际上可能使用 super
的任何其他模块) 在包含在 RSpec::Core::ExampleGroup
中之前被包含在示例组中。我曾经考虑过在 RSpec::Core::ExampleGroup
或 RSpec::Matchers
中添加一个钩子来检测用户何时包含它,并以某种方式阻止或警告他们。但是,我很快意识到 Ruby 模块系统的极端灵活性使这变得非常复杂。用户可能没有直接包含 RSpec::Matchers
;他们可能正在包含来自某些库或插件的模块,该模块本身包含 RSpec::Matchers
,或者包含包含 RSpec::Matchers
的模块。我意识到这将不是一个简单的解决方案,无法立即解决。
相反,在与 David Chelimsky 和 RSpec 核心团队的其他成员交谈后,我们决定最好更改包含 RSpec::Matchers
的时间。通过在 RSpec::Core::ExampleGroup
被子类化之前在 RSpec::Core::ExampleGroup
中包含 RSpec::Matchers
,该问题将完全消失。
我们仍然希望让人们配置 expect_with :stdlib
。我们能想到的最佳解决方案是将期望模块的包含延迟到 RSpec::Core::ExampleGroup
首次被子类化的那一刻。这使用户有机会配置 expect_with :stdlib
,只要他们在定义任何示例组之前这样做。一旦示例组被定义,我们就会自动默认使用 expect_with :rspec
——这意味着任何未来的 expect_with
配置将被有效地忽略。
这是提交内容,如果您感兴趣。您会注意到它还更改了模拟框架适配器模块的包含时间。由于我们不想在模拟框架适配器模块之一使用 super
时再次处理此问题,我认为最好也更改它。
整个问题让我觉得,允许用户在定义示例后配置 RSpec 是有问题的。在我看来,由于配置会影响 RSpec 的工作方式,因此最好在开始使用 RSpec (即通过定义示例) 之前设置它。我 做了一个更改,在定义示例组后使用 RSpec.configure
时会导致 RSpec 打印弃用警告。
回想起来,我应该意识到这会影响很多用户。当时,我没有想到这一点。我所有的 RSpec 配置都是在定义任何示例组之前完成的,我假设其他人也是这样做的。弃用警告主要是在有人可能在定义示例之后配置 RSpec 的情况下添加的。
RSpec 2.6
RSpec 2.6 发布了这些更改,我们立即开始收到有关此警告的问题和投诉。有些人,例如 Gary Bernhardt 和 Corey Haines,有一种通过尽可能少地加载来加速测试的技术——这通常涉及不从每个规范文件中加载 spec_helper
。当一个规范文件 (比如 spec/lib/my_class_spec.rb
) 不需要 spec_helper
,但同一套中的另一个文件 (比如 spec/models/user_spec.rb
) 需要时,这会触发弃用警告。如果 spec/lib/my_class_spec.rb
在 spec/models/user_spec.rb
之前加载 (通常会发生这种情况——它们倾向于按字母顺序加载),它将触发警告,因为示例在 spec/models/user_spec.rb
中定义,而配置是在 spec_helper.rb
中发生的。
我现在是“不要加载 spec_helper”方法的忠实粉丝,但在做出更改时,我从未听说过或想过要这样做。
我们在一个 rspec-rails 问题 上讨论了这个问题。由此产生的最佳建议 (也是没有人反对的建议) 是删除警告,而是如果在定义示例组后设置特定的有问题的配置 (expect_with
和 mock_with
),则引发错误。
我做出了 这项更改,它在 RSpec 2.7 中发布。
RSpec 2.7
RSpec 2.7 发布后,又出现了更多关于此更改的投诉。现在,一些用户由于错误而无法运行他们的规范。实际上,这使得问题变得更糟——之前的警告可以忽略,但错误不能忽略。
我几天前 提交了一个更改,应该可以改善这种情况:与其在定义示例组后每次调用 expect_with
或 mock_with
时引发错误,不如只在方法调用更改设置时引发错误。如果您只是显式设置默认值 (即 mock_with :rspec
) 或重新设置现有配置值,则不会引发错误。
这无疑是一个重要的、必要的更改,我只是在进行之前的更改时没有考虑到它。我为此道歉。
请注意,这并没有完全消除错误:如果您正在将 RSpec 配置为使用不同的期望/断言框架或模拟框架,那么这仍然必须在定义示例组 *之前* 完成,以便 RSpec 可以 *在* RSpec::Core::ExampleGroup
被子类化之前将适当的模块包含到 RSpec::Core::ExampleGroup
中。
避免这些警告/错误
如果您使用的是 RSpec 2.6 或 2.7 并收到了这些警告或错误,您可以进行一些非常简单的更改来避免它们。
首先,确保您所有的 spec_helper require 只是 require 'spec_helper'
。如果您使用相对于 __FILE__
的路径(就像人们经常做的那样),spec_helper.rb 可以被多次加载(因为 ruby 会很乐意重新 require 一个文件,如果它被指定为不同的文件路径)。RSpec 会为您将规范目录放到加载路径上,因此您可以 (而且通常应该) 只是 require spec_helper
。
其次,如果您遵循“不要加载 spec_helper”方法,并且您需要配置 expect_with
或 mock_with
,则需要为隔离的、快速的规范创建一个辅助文件。
这是一个方法
# spec/fast_spec_helper.rb
require 'rspec'
RSpec.configure do |c|
c.mock_with :mocha
end
# spec/spec_helper.rb
require 'fast_spec_helper'
# load rails or whatever to get your full app environment booted
RSpec.configure do |c|
# other RSpec configuration
end
# spec/lib/my_class_spec.rb
require 'fast_spec_helper'
describe MyClass do
end
# spec/controllers/my_controller_spec.rb
require 'spec_helper'
describe MyController do
end
我知道这有点违背了“不要加载 spec_helper”方法,但重要的是,rails/sinatra/任何环境都没有在隔离的规范中完全加载。我们使用 fast_spec_helper.rb 来仅配置最少的配置——必须在定义示例组之前设置的特定 expect_with
或 mock_with
设置。一个额外的 require 不会对隔离测试的速度产生明显的影响。
当然,如果您只是将 mock_with
或 expect_with
设置为默认值 (:rspec
),那么您应该完全删除该配置,错误应该会消失——不需要单独的 fast_spec_helper.rb
文件。
或者,您可以使用 ruby 的 -r
标志强制在任何隔离规范之前加载辅助文件,而不是必须从每个规范文件中 require 辅助文件。
有没有更好的方法来解决这个 Bug?
所以,这就是关于最近 RSpec 中配置警告和错误的全部故事。对于由此带来的升级困扰,我深表歉意。我尽我所能地解决了这些问题。
如果您认为我彻底搞砸了,或者您能想到更好的方法来处理 ruby 1.9 的 bug,请在评论中告诉我!
此外,我在 Ruby 问题追踪器上提交了一个 bug 报告。如果您想看到它被修复,请在该处评论。