【问题标题】:What's the difference between RSpec's subject and let? When should they be used or not?RSpec 的 subject 和 let 有什么区别?什么时候应该使用它们?
【发布时间】:2016-11-21 02:05:34
【问题描述】:

http://betterspecs.org/#subject 有一些关于subjectlet 的信息。但是,我仍然不清楚它们之间的区别。此外,SO 帖子What is the argument against using before, let and subject in RSpec tests? 说最好不要使用subjectlet。我该去哪里?我很困惑。

【问题讨论】:

    标签: ruby unit-testing rspec


    【解决方案1】:

    总结:RSpec 的主题是一个特殊的变量,指的是被测试的对象。可以对其隐式设置期望,这支持单行示例。在某些惯用的情况下,读者很清楚,但在其他方面很难理解,应该避免。 RSpec 的 let 变量只是延迟实例化(记忆化)的变量。它们并不像主题那样难以理解,但仍可能导致复杂的测试,因此应谨慎使用。

    主题

    工作原理

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

    默认情况下,如果最外层示例组(describecontext 块)的第一个参数是一个类,则 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

    无论主题如何定义,

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

    2. 可以对其隐式设置期望(无需写入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
    

    【讨论】:

    • 哇! +?!可以这么说,“每个例子的单一期望”是物超所值的。我从来没有觉得有必要非常严格地遵守那个(错误)规则,但现在我有充分的武器来对付那些试图把它强加给我们其他人的人。谢谢!
    • 另外,如果您希望您的多期望块运行所有期望行(而不是在第一个失败时不运行),您可以在类似it ​"marks a task complete"​, ​:aggregate_failures​ ​do 的行中使用:aggregate_failures 标签(采取来自Rails 5 Test Prescriptions一书)
    • 我也完全反对炒作和恋物癖,“每个示例单个期望”主要适用于那些喜欢将许多不相关的期望组合在一个示例中的人。从语义上讲,您的示例不是理想的,因为它不验证单个数字,它验证 [0, 9] 中的实数 - 令人惊讶的是,可以将其编码为更具可读性的单个期望@ 987654367@.
    • 如果您只想验证一位数字(Int [0,9]),那么expect(result.to_s).to match(/^[0-9]$/) 会更合适 - 我知道这很丑,但它确实测试了您所说的,或者也许,使用between + is_a? Integer,但在这里你也在测试类型。只是let.. 它不应该成为关注的对象,实际上重新评估示例之间的值可能会更好。否则为帖子 +1
    • 我喜欢你对let! 危险性的解释,我还没有亲自说服我的队友。我要把这个答案发过去。
    【解决方案2】:

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

    Subject 允许您声明一个测试主题,然后在随后的任意数量的测试用例中重用它。这减少了代码重复(DRYing up your code)

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

    【讨论】:

      【解决方案3】:

      subject 是被测试的对象,通常是一个实例或一个类。 let 用于在测试中分配变量,这些变量是惰性评估的,而不是使用实例变量。这个帖子中有一些很好的例子。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-04-17
        • 2010-09-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-12-20
        • 2021-08-10
        • 2010-09-29
        相关资源
        最近更新 更多