RSpec的主题与let之间有什么区别? 什么时候应该使用它们?

http://betterspecs.org/#subject有一些关于subjectlet 。 但是,我仍然不清楚它们之间的区别。 此外,SOpost反对在RSpec测试中使用之前,之后和主题的论点是什么? 说不要使用任何一个subjectlet更好。 我该去哪儿? 我感到很困惑。

简介:RSpec的主题是一个特殊变量,指的是被测对象。 可以隐含地设置期望,这支持单行示例。 读者在一些惯用的案例中很清楚,但其他方面很难理解,应该避免。 RSpec的let变量只是懒惰的实例化(memoized)变量。 它们并不像主题那样难以理解,但仍然可能导致纠结的测试,因此应谨慎使用。

主题

这个怎么运作

主题是被测试的对象。 RSpec对该主题有明确的概念。 它可能定义也可能不定义。 如果是,RSpec可以调用它上面的方法而不需要明确引用它。

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

 class A end describe A do it "is instantiated by RSpec" do expect(subject).to be_an(A) end end 

您可以使用subject定义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

然而,主题是定义的,

  1. 它被懒惰地实例化了。 也就是说,在示例中引用subject或命名主题之前,不会发生所描述的类的隐式实例化或传递给subject的块的执行。 如果你希望你的explict主题被急切地实例化(在其组中的一个例子运行之前),请说subject! 而不是subject

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

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

    该主题用于支持这种单行语法。

何时使用它

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

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

因此, 当所有读者都很好地理解上下文并且实际上不需要示例描述时,使用隐式主题是有帮助的 。 规范案例是使用shoulda匹配器测试ActiveRecordvalidation:

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

一个明确的匿名subject (用没有名字的subject定义)稍好一些,因为读者可以看到它是如何被实例化的,但是

  • 它仍然可以将主题的实例化远离它所使用的位置(例如,在具有许多使用它的示例的示例组的顶部),这仍然很难遵循,并且
  • 它有隐含主题所做的其他问题。

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

因此, 明确的匿名subject或命名主题的合法使用是非常罕见的

let变量

他们如何工作

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

  • 他们用let / let!定义let! 而不是subject / subject!
  • 他们没有设置匿名subject或允许隐含地调用期望。

何时使用它们

使用let来减少示例之间的重复是完全合法的。 但是,只有在不牺牲测试清晰度的情况下才这样做。 最安全的使用时间是当let变量的目的从其名称中完全清楚时(因此读者不必找到定义,可能需要很多行来理解每个示例)并且它在在每个例子中都是一样的。 如果其中任何一个不成立,请考虑在普通旧局部变量中定义对象或在示例中调用工厂方法。

let! 风险很大,因为它不是懒惰的。 如果有人将示例添加到包含let!的示例组中let! ,但这个例子不需要let! 变量,

  • 那个例子很难理解,因为读者会看到这个let! 变量并想知道它是否以及如何影响这个例子
  • 由于创建let!所需的时间,示例将比它需要的速度慢let! variablle

所以使用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 

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

Subject允许您声明一个测试主题,然后在之后的任何数量的后续测试用例中重复使用它。 这减少了代码重复(干掉你的代码)

Let替代before: each块,它们将测试数据分配给实例变量。 Let您获得一些优势。 首先,它将值缓存而不将其赋值给实例变量。 其次,它被懒惰地评估,这意味着它在规范要求之前不会被评估。 这样可以帮助您加快测试速度。 我也认为let更容易阅读

subject是被测试的,通常是实例或类。 let用于在测试中分配变量,这些变量是懒惰地使用实例变量进行评估的。 这个post中有一些不错的例子。

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