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


Answers:


186

简介:RSpec的使用者是一个特殊变量,它指向要测试的对象。可以在其上隐式设置期望,它支持单行示例。在某些惯用案例中,读者很容易理解,但否则很难理解,应避免使用。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

无论主题如何定义

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

  2. 可以对其隐式设置期望(无需书写subject或指定主题的名称):

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

    存在该主题以支持这种单行语法。

什么时候使用

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

  • 它在后台实例化。
  • 无论是隐式使用(通过is_expected不带显式接收器的调用)还是显式使用(如subject),它都不会向读者提供有关在其上调用期望的对象的角色或性质的信息。
  • 单线示例语法没有示例说明(it普通示例语法中的string参数),因此读者唯一了解示例目的的信息就是期望本身。

因此,仅当上下文可能被所有读者很好地理解并且确实不需要示例描述时,使用隐式主题才有帮助。规范的案例是使用比对匹配器测试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

17
哇!+💯!可以这么说,“单身预期的恋物癖”是值得的。我从来没有觉得需要非常严格地遵循(错误)规则,但是现在我有足够的力量反对那些试图将其强加给我们其他人的人们。谢谢!
iconoclast

2
另外,如果您希望多期望模块运行所有预期行(而不是在第一个失败时不运行),则可以:aggregate_failures在行中使用标记it ​"marks a task complete"​, ​:aggregate_failures​ ​do(摘自Rails 5 Test Prescriptions)
迷宫

1
我也都反对炒作和迷恋,“每个示例一次期望”主要适用于那些希望将许多无关的期望归为一个示例的人。从语义上讲,您的示例不是理想的,因为它不对单个数字进行验证,而是对[0,9]中的实数进行验证-令人惊讶的是,可以将其编码为更具可读性的单个期望expect(result).to be_between(0, 9)
安德烈·菲盖雷多

如果您只想验证一位数字(Int [0,9]),那将更适合expect(result.to_s).to match(/^[0-9]$/)-我知道这很丑陋,但实际上可以测试您在说什么,或者使用between+ is_a? Integer,但是在这里您正在测试类型也一样。而且,let它不应该成为关注的对象,而实际上重新评估示例之间的值可能会更好。否则,该职位+1
Andre Figueiredo

我喜欢您对危险的解释,但let!并没有自己说服我的队友。我将发送此答案。
戴蒙·aw

5

Subjectlet只是工具来帮助你整理和加快你的测试。rspec社区中的人们确实在使用它们,因此我不必担心是否可以使用它们。它们的用法类似,但用途略有不同

Subject允许您声明一个测试主题,然后在以后的许多测试案例中重用它。这样可以减少代码重复(干燥代码)

Letbefore: each块的替代方法,后者将测试数据分配给实例变量。 Let给您带来几个好处。首先,它缓存该值而不将其分配给实例变量。其次,它是惰性评估的,这意味着直到规范要求它才会得到评估。从而let帮助您加快测试速度。我也认为let更容易阅读


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.