ruby RSpec的subject和let有什么区别?什么时候应该使用或不使用?

ar5n3qh5  于 2023-08-04  发布在  Ruby
关注(0)|答案(4)|浏览(118)

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

0s0u357o

0s0u357o1#

摘要:RSpec的subject是一个特殊的变量,它引用被测试的对象。可以隐式地对它设置期望,这支持单行示例。在某些惯用情况下,读者可以清楚地看到它,但在其他情况下,它很难理解,应该避免。RSpec的let变量只是延迟示例化(memoized)变量。它们不像主题那样难以理解,但仍然会导致混乱的测试,因此应该谨慎使用。

主题

工作原理

主体是被测试的对象。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
1.可以隐式地对它设置期望值(不需要写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

(This是一个简单的方法示例,它返回一个我们需要两个期望值的数字,但是如果方法返回一个更复杂的值,需要许多期望值和/或有许多副作用,都需要期望值,这种风格可以有更多的示例/期望值。
人们这样做是因为他们听说每个示例应该只有一个期望(这与每个示例应该只测试一个方法调用的有效规则相混淆),或者因为他们喜欢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

a5g8bdjr

a5g8bdjr2#

Subjectlet只是帮助您整理和加速测试的工具。rspec社区的人确实使用它们,所以我不会担心是否可以使用它们。它们的用法相似,但用途略有不同
Subject允许您声明一个测试主题,然后在随后的任意数量的测试用例中重用它。这减少了代码重复(DRYing up your code)
Letbefore: each块的替代方案,它将测试数据分配给示例变量。Let为您提供了一些优势。首先,它缓存值而不将其分配给示例变量。第二,它是延迟计算的,这意味着直到规范调用它时才计算它。因此,let可以帮助您加快测试速度。我也认为let更容易阅读

xvw2m8pv

xvw2m8pv3#

subject是测试对象,通常是一个示例或类。let用于在测试中分配变量,这些变量是惰性计算的,而不是使用示例变量。在这个帖子里有一些很好的例子。
https://github.com/reachlocal/rspec-style-guide/issues/6

fnx2tebb

fnx2tebb4#

实现几乎相同

Subject和let具有不同的语义,但它们具有几乎相同的实现。然而,主语可以是隐含的:

# These are all equivalent
it { should ____ }
it { is_expected.to ____ }
it { expect(subject).to ____ }

字符串
实验对象就是你要测试的东西。
定义将在测试(或测试设置等)中使用的对象
let!将不会延迟运行,subject!也是如此。您可以使用它们来创建记录等。即使变量不会被显式引用。

let!(:post) { create :post, user: subject }
subject { create :user }
it { should be_valid } # subject.valid? should be true
it { should have_attributes(posts?: true) } # subject.posts? should be true


虽然丹有上述的一些理由,但他是少数人认为使用隐含主语是一种不好的做法。(https://www.betterspecs.org/#subject)。Rails是一个充满魔力的东西,而“每个示例一个期望”的迷信有一个好处,那就是可以准确地显示失败的地方。

应该用来定义对象,而不是调用方法

Subject和let都应该是对象,通常不是方法调用。它们是惰性计算的,但也是记忆的,每个示例只运行一次。

我使用的一个异常

我个人,有时会打破上述规则,只有当这样做。

# This is the EXCEPTION not the rule.
# Typically subject should be an object, like let,
# NOT something with side-effects.
subject do
  post posts_path(title: "Example", body: "Example post body")
end
it { expect { subject }.to change(Post, :count).by(1) }
it { expect { subject }.to change(User, :count).by(0) }


它利用了即使是主体也是懒惰的这一事实。

相关问题