总结:RSpec 的主题是一个特殊的变量,指的是被测试的对象。可以对其隐式设置期望,这支持单行示例。在某些惯用的情况下,读者很清楚,但在其他方面很难理解,应该避免。 RSpec 的 let 变量只是延迟实例化(记忆化)的变量。它们并不像主题那样难以理解,但仍可能导致复杂的测试,因此应谨慎使用。
主题
工作原理
主题是被测试的对象。 RSpec 对这个主题有一个明确的想法。它可能会或可能不会被定义。如果是,RSpec 可以在不显式引用的情况下调用它的方法。
默认情况下,如果最外层示例组(describe 或 context 块)的第一个参数是一个类,则 RSpec 创建该类的一个实例并将其分配给主题。比如下面的pass:
class A
end
describe A do
it "is instantiated by RSpec" do
expect(subject).to be_an(A)
end
end
你可以用subject自己定义主题:
describe "anonymous subject" do
subject { A.new }
it "has been instantiated" do
expect(subject).to be_an(A)
end
end
您可以在定义主题时为其命名:
describe "named subject" do
subject(:a) { A.new }
it "has been instantiated" do
expect(a).to be_an(A)
end
end
即使您命名主题,您仍然可以匿名引用它:
describe "named subject" do
subject(:a) { A.new }
it "has been instantiated" do
expect(subject).to be_an(A)
end
end
您可以定义多个命名主题。最近定义的命名主题是匿名subject。
无论主题如何定义,
它是惰性实例化的。也就是说,所描述的类的隐式实例化或传递给subject 的块的执行直到subject 或在示例中引用命名的主题才会发生。如果您希望您的显式主题被急切地实例化(在其组中的示例运行之前),请说 subject! 而不是 subject。
-
可以对其隐式设置期望(无需写入subject 或指定主题的名称):
describe A do
it { is_expected.to be_an(A) }
end
主题的存在是为了支持这种单行语法。
什么时候使用
隐含的subject(从示例组推断)很难理解,因为
- 它在幕后被实例化。
- 无论是隐式使用(通过调用
is_expected 而不使用显式接收器)还是显式使用(如subject),它都不会向读者提供有关调用期望的对象的角色或性质的信息。
- 单行示例语法没有示例描述(普通示例语法中
it 的字符串参数),因此读者对示例目的的唯一信息是期望本身。
因此,只有在上下文可能被所有读者很好理解并且确实不需要示例描述时,才使用隐含的主题。典型案例是使用 shoulda 匹配器测试 ActiveRecord 验证:
describe Article do
it { is_expected.to validate_presence_of(:title) }
end
显式匿名subject(用不带名称的subject 定义)要好一些,因为读者可以看到它是如何实例化的,但是
- 它仍然可以将主题的实例化放置在远离它使用的位置(例如,在有许多使用它的示例的示例组的顶部),这仍然很难遵循,并且
- 它还有隐式主语的其他问题。
命名主题提供了一个意图揭示的名称,但使用命名主题而不是 let 变量的唯一原因是如果您想在某些时候使用匿名主题,我们刚刚解释了为什么匿名主题主题很难理解。
因此,合法使用显式匿名 subject 或命名主题的情况非常罕见。
let变量
它们是如何工作的
let 变量就像命名的主题,除了两个不同之处:
- 它们被定义为
let/let! 而不是subject/subject!
- 他们不设置匿名
subject 或允许对其隐式调用期望。
何时使用它们
使用let 来减少示例之间的重复是完全合法的。但是,只有在不牺牲测试清晰度的情况下才这样做。 使用let 的最安全时间是当let 变量的用途从其名称中完全清楚时(这样读者就不必找到定义,可能在很多行之外,以理解每个示例),并且在每个示例中都以相同的方式使用它。如果其中任何一个不正确,请考虑在普通的旧局部变量中定义对象或在示例中直接调用工厂方法。
let! 是有风险的,因为它不是懒惰的。 如果有人将示例添加到包含 let! 的示例组,但该示例不需要 let! 变量,
- 该示例将难以理解,因为读者会看到
let! 变量并想知道它是否以及如何影响示例
- 示例将比它需要的慢,因为创建
let! 变量需要时间
所以使用let!,如果有的话,只能在小的、简单的示例组中使用,这样未来的示例编写者不太可能陷入该陷阱。
对每个例子的单一期望恋物癖
主题或let 变量的普遍过度使用值得单独讨论。有些人喜欢这样使用它们:
describe 'Calculator' do
describe '#calculate' do
subject { Calculator.calculate }
it { is_expected.to be >= 0 }
it { is_expected.to be <= 9 }
end
end
(这是一个简单的方法示例,它返回一个我们需要两个期望的数字,但是如果该方法返回一个需要许多期望和/或有许多期望的更复杂的值,这种风格可以有更多的示例/期望所有需要预期的副作用。)
人们这样做是因为他们听说每个示例应该只有一个期望(这与每个示例只应该测试一个方法调用的有效规则相混淆)或者因为他们爱上了 RSpec 技巧.不要这样做,无论是匿名或命名主题还是let 变量!这种风格有几个问题:
- 匿名主题不是示例的主题——方法是主题。以这种方式编写测试会搞砸语言,让人更难思考。
- 与以往的单行示例一样,没有任何空间可以解释期望的含义。
- 必须为每个示例构建主题,这很慢。
相反,写一个例子:
describe 'Calculator' do
describe '#calculate' do
it "returns a single-digit number" do
result = Calculator.calculate
expect(result).to be >= 0
expect(result).to be <= 9
end
end
end