What's the difference between RSpec's subject and let? When should they be used or not?

http://betterspecs.org/#subject has some info about subject and let. However, I am still unclear on the difference between them. Furthermore, the SO post What is the argument against using before, let and subject in RSpec tests? said it is better to not use either subject or let. Where shall I go? I am so confused.

54984 次浏览

subject是正在测试的内容,通常是一个实例或类。let用于在测试中分配变量,这些变量的计算采用延迟方式,而不是使用实例变量。这个线程中有一些很好的例子。

https://github.com/reachlocal/rspec-style-guide/issues/6

Subjectlet只是帮助您整理和加速测试的工具。Rspec 社区的人确实使用它们,所以我不会担心是否可以使用它们。它们可以类似地使用,但用途略有不同

Subject允许您声明一个测试主题,然后在以后的任意数量的测试用例中重用它。这样可以减少代码重复(对代码进行 DRY 处理)

Letbefore: each块的替代品,before: each块将测试数据分配给实例变量。Let给了你一些优势。首先,它缓存值而不将其分配给实例变量。其次,它是延迟计算的,这意味着在规范调用它之前它不会被计算。因此,let可以帮助您加快测试速度。我也认为 let更容易阅读

摘要: RSpec 的 subject 是一个特殊的变量,它引用被测试的对象。可以隐式地在它上面设置期望值,这支持一行示例。在一些惯用的情况下,它对读者来说是清楚的,但在其他情况下是难以理解的,应该避免。RSpec 的 let变量只是延迟实例化(制表)变量。它们并不像受试者那样难以理解,但仍然会导致复杂的测试,因此应谨慎使用。

主题

它是如何工作的

主体是被测试的对象。RSpec 对主题有明确的想法。它可能被定义,也可能不被定义。如果是,RSpec 可以在不显式引用它的情况下调用它的方法。

默认情况下,如果最外层示例组(describecontext块)的第一个参数是一个类,则 RSpec 将创建该类的一个实例并将其分配给主题。例如,以下通过:

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

However the subject is defined,

  1. 它被懒惰地实例化了。也就是说,只有在示例中引用 subject或指定的主题时,才会隐式实例化所描述的类或执行传递给 subject的块。如果您希望您的显式主题被热切地实例化(在其组中的一个示例运行之前) ,可以使用 subject!而不是 subject

  2. 可以隐式地对其设置期望值(不需要编写 subject或命名主题的名称) :

    describe A do
    it { is_expected.to be_an(A) }
    end
    

    主题的存在就是为了支持这种一行语法。

什么时候用

隐式 subject(从示例组推断出的)很难理解,因为

  • 它是在幕后实例化的。
  • 无论是隐式使用(通过在没有显式接收方的情况下调用 is_expected)还是显式使用(作为 subject) ,它都没有向读者提供关于期望被调用的对象的角色或性质的信息。
  • 一行程序的示例语法没有示例描述(常规示例语法中 it的字符串参数) ,因此读者所知道的关于示例用途的唯一信息就是期望本身。

因此,it's only helpful to use an implicit subject when the context is likely to be well understood by all readers and there is really no need for an example description.canonical 案例正在使用应该匹配器测试 ActiveRecord 验证:

describe Article do
it { is_expected.to validate_presence_of(:title) }
end

显式匿名 subject(使用没有名称的 subject定义)更好一些,因为读者可以看到它是如何实例化的,但是

  • 它仍然可以把主题的实例化放在远离它被使用的地方(例如,在一个有许多使用它的例子的示例组的顶部) ,这仍然是很难遵循的,并且
  • 它还有隐含主体的其他问题。

命名主题提供了一个意图揭示的名称,但是使用命名主题而不是 let变量的唯一原因是,如果你想在某些时候使用匿名主题,我们刚刚解释了为什么匿名主题很难理解。

那么,合法使用显式匿名 subject或命名主题的情况非常少见

let变量

它们是如何工作的

let变量就像命名的主题一样,除了两个不同之处:

  • they're defined with let/let! instead of 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