新手问题 RSpec 中 let 和 subject 的区别是什么?

zhulinpinyu · 2013年03月09日 · 最后由 chalvern 回复于 2017年06月19日 · 14154 次阅读

rspec 中 let 和 subject 的区别是什么?有哪为大牛帮助解释解释。谢谢

1 楼 已删除

好吧,我先说有关提问的两点:

  • 我觉得这样的一个问题,用不着呼叫那么多大牛们来解释的... 杀鸡焉用牛刀?
  • 这样的问题完全可以自己 Google 找到答案。

最后我还是回答好了,我不是大牛,我适合回答这样的问题。

let 以及 subject 是类似于 Minitest 中的 before 的简写形式

subject { ... } 定义了一个叫做 subject 的方法,它返回了代码块内定义的那个对象. let(:a) { ... } 定义了一个叫做 a 的方法,他也是返回了代码块内定义的那个对象。

#3 楼 @nightire 神回复,收藏!两个方法被你挖掘到如此深度,且读罢让人豁然开朗!赞一个!

#1 楼 @zhulinpinyu 非常能理解你第一次见到 letsubject 的时候会困惑,我第一次也弄不明白,如果不是自己写亲自动手多写一些这样的测试代码,仅仅通过文档阅读和看其他人的代码,是很难掌握这两个方法的神髓的,所以建议你照着 rspec 文档 1 文档 2 上的例子亲自敲一遍。这里要再一次赞一下 @nightire 深挖源码的做法,真的是一个好习惯,能帮你深入理解 DSL 语法糖这一表层抽象之下的细节是什么,上帝隐藏在细节之中。

遇到问题其实不用 AT 大牛(我必须承认你 AT 的几个人中我不是大牛)。如果大家都用 AT 大牛的方式,那么没有被你 AT 的人怎么会乐意帮助你,回答你的问题呢?更不会出现三楼这种神回复了。

@nightire 米卢说过“态度决定一切”赞一个

#3 楼 @nightire 非常感谢您这段详细的分析,豁然开朗。

#4 楼 @lgn21st 初次提问,只是很迫切的想要知道答案,google 无果,故 AT 几位大牛。非常感谢您以及@zw963 的建议和指正。同时再次感谢 @nightire 的神回复 赞一个

#3 楼 @nightire 3 楼的回答强有力的说明,论坛的评论价值并不亚于主贴,受教了

#3 楼 @nightire

探索精神赞一个~~

不过,是不是有点把简单问题复杂化了?是不是站在测试的角度来分析这两个东西,更清晰? 我觉得似乎我的答案还是好懂点?? 呵呵。

有几个问题:

# 使用 subject
describe CheckingAccount, "with $50" do
# 直接用的 Class Name,若此时没有显式定义 subject,那么默认的 subject 就是 CheckingAccount,可通过在代码中输出 subject 获知
  subject { CheckingAccount.new(Money.new(50, :USD)) }
  it { should have_a_balance_of(Money.new(50, :USD)) }
end

你注释是什么意思?如果没有定义 subject { ... }, 默认就是 CheckingAccount, 这默认怎么知道如何去初始化对象,难道会解析你那个 "with $50" ??

# 重构上面的例子
describe "Checking Account initialization" do
  let(:starting_balance) { Money.new(50, :USD) }
  subject(:account) { CheckingAccount.new(Money.new(50, :USD)) }

  it "has $50 balance" do
    expect(account).to have_a_balance_of(starting_balance)
  end

  it "has a balance attribute which equals the starting balance" do
    expect(account.balance).to eq(starting_balance)
  end
end

我就最后这个看得最清晰了,不过,我没看出来 这个里面 let 和 subject 有啥区别? 这种情况为什么不用两个 let ?

#12 楼 @zw963

第一个问题其实 @nightire 解释的非常准确,且给出的例子中的注释也没有问题,不过为了帮助你理解,我给你贴一个更加直观的代码。

describe Post do
  # 这里没有定义 subject,所以默认就是 Post, 而且不需要去初始化对象,因为测试的是 Post 本身而不是它的实例。
  # 出处: https://github.com/thoughtbot/shoulda
  it { should belong_to(:user) }
  it { should validate_presence_of(:title) }
end

第二个问题,不能仅仅按照学究的方式从语言和功能定义角度去理解,而是要从 DSL 帮助增强代码的表达能力角度来理解。这里的 subjectlet 都是通过尽量用英文的自然语法来加强代码的语义表达能力,subject 就是英文语法中的主语成分, it 在这里所指代的正是主语,当 it 和 block 中的 subject 同时出现的时候,目的是为了增强代码的语义可读性。let 是定义 helper 方法的另外一种表达方式,并且有 lazy-evaluated 的优点:

# 重构上面的例子
describe "Checking Account initialization" do
  # let 可以改写成
  # def sharting_balance; Money.new(50, :USD); end
  let(:starting_balance) { Money.new(50, :USD) }
  subject { CheckingAccount.new(Money.new(50, :USD)) }

  it "has $50 balance" do
    expect(subject).to have_a_balance_of(starting_balance)
  end

  it "has a balance attribute which equals the starting balance" do
    expect(subject.balance).to eq(starting_balance)
  end
end

#14 楼 @nightire

基本上看明白了你讲的语义,不过我觉得 #13 楼 @lgn21st 讲的更简单明了一些?

不过我也看出来一点:不是你们把简单问题复杂化,要怪只能怪 RSpec 做的太臃肿了。 这个观点不是我的,而是 MiniTest 的作者的。我只是转述一下。(貌似他是 Rspec 前作者之一)

下面是这两个方法在 MiniTest 的源码,通过这个源码就不难理解,为什么我的想法和你不太一样了。

##
# Essentially, define an accessor for +name+ with +block+.
#
# Why use let instead of def? I honestly don't know.

def self.let name, &block
  define_method name do
    @_memoized ||= {}
    @_memoized.fetch(name) { |k| @_memoized[k] = instance_eval(&block) }
  end
end

##
# Another lazy man's accessor generator. Made even more lazy by
# setting the name for you to +subject+.

def self.subject &block
  let :subject, &block
end

MiniTest 的约定很简单:

  1. 一个上下文中只应该有一个主题,所以,subject 干脆不提供参数。也不要这个 account, 那个 product 了,干脆大家都用 subject. 因为他就是 主题.
  2. 除了主题之外,其他东西用 let 提供。当然你非要全部重命名 (不用 subject, 使用更加贴切的名字), 那就全部用 let 好了。

#13 楼 @lgn21st 明白了默认 subject 的含义... (不过,那个例子中应该是 belongs_to ? )

另外,从源码中来看,有关 lazy-evaluated, let 和 subject 没有什么区别的。他们都是 lazy 的. 或者说:他们都是通过 block 方式实现了 lazy_evaluated. 代码中体现出来的只是,对象是通过一个上下文中的全局 Hash 被 Cached, 当复用时,无需反复创建对象。

我觉得 RSpec 大家都用的原因是:UnitTest 太慢了... 不过,现在的 MiniTest 已经比 RSpec 快很多了,现在已经 Ruby 2.0 & Rails 4 时代了,给个建议,大家是否可以考虑了解下 MiniTest ??

@nightire 连续好几个神回复…+1024

describe Array do
  context 'when first created' do # 使用 context 的好处:语法上清除作用域;语义上体现语境变化
    # 主角刚诞生,两手空空...
    it { should have(0).items } # => pass
    its(:size) { should == 0 } # => pass
  end
  ……

这时候的 subject 应该是 Array,而不是 Array 的实例呀,Array 怎么会有 items、size ?

@zw963 我喜欢 RSpec,不是因为 TestUnit 太慢,而是 @nightire 提到的可读性,RSpec 写起来的愉悦感,比 TestUnit 那一堆 assert 要好多了

#16 楼 @zw963 不是你的想法和我的不太一样,而是这两个框架不一样,你可能只熟悉 MiniTest,我则因为两者都用,所以对语义在 RSpec 中的作用更熟悉些。比较一下二者,最贴切的一句话就是:RSpec 是 DSL,MiniTest 是 Ruby;对于 Ruby 开发者来说,MiniTest 足够了,但是 RSpec 可以写出即使不懂 Ruby 语言也能看得懂的 specs(the new feature specs for example, which is my favorite improvement)。

这其实跟什么时代没关系,如果我在写一个小型项目,参与者都是 Ruby 程序员的时候,我们就用 MiniTest,因为在此种情形下,subject 和 let 有没有区别不重要,大家心里都清楚。而在这个帖子里,我写得复杂不是为了给某个人看的,而是给所有初学者解释 RSpec 使用 DSL 对我们意味着什么,能让我们获得什么。

#17 楼 @keating 是 Array 的实例,等同于 Arrar.new 一个。看了你的疑问我忽然反应过来,我在第一篇回答里解释默认 subject 时为什么让人看不懂了,因为我漏了 .new ……

@nightire 第一篇倒是看懂了,因为你在 describe 中又写了 subject @doitian 原来如此……

#16 楼 @zw963 为什么是 Rspec 而不是 MiniTest,选择的理由是看性能?还是代码是否臃肿?这些其实都是表象。对我来说,一个简单工具和一个复杂工具放到一起的时候,不一定是越简单就一定越好,要看你要解决的问题的复杂度(其实更多的时候是你对你要解决的问题的复杂度理解的维度),MiniTest 能给你的东西从他的名字就能看出来 —— Mini。当你要测试的目标以及环境复杂多变的时候,RSpec 给了你足够的支撑(单独写的话,会是一篇长文,试着用一小段概括一下)。

RSpec 的背后除了 subject, let 之外东西还有很多,不过对我来说最有价值的是开发者对测试方法学层面的不断探索,挖掘,以及将成果沉淀这个框架工具中。翻出 2008 年我刚刚接触 RSpec 的时候的文章,哪个时候我从 UnitTest 转向 RSpec,最困扰我的并不是语法,而是 BDD 这个概念,当接受了 BDD 思想之后,你会发现你开始不愿意回去 UnitTest 了。虽然现在的 MiniTest 通过语法扩充,也能写出 RSpec 的 describe … it …. 这样形似的代码,但问题是,MiniTest 作为 Ruby 的标准库也就止步于此了。两者一起用下来,体验根本不是那么回事。RSpec 并未止步于一个 BDD 框架,它还提供极为优秀的 Mocks 套件,灵活的扩展机制让你方便的编写扩展插件,比如 Shuda ,推动 Acceptance Test Driven Planning 并衍生出 Cucumber 去实现 Full Stack Agile Methodology 等等...

关于 belong_to 的问题,是 Shuda 提供的。

#21 楼 @doitian

describe Post do
  # 这里没有定义 subject,所以默认就是 Post, 而且不需要去初始化对象,因为测试的是 Post 本身而不是它的实例。
  # 出处: https://github.com/thoughtbot/shoulda
  it { should belong_to(:user) }
  it { should validate_presence_of(:title) }
end

那么这个呢?到底是 Post.new 还是 Post ?

#24 楼 @zw963 p 一下就知道了

#25 楼 @doitian

MiniTest 当中,是一个对象的 eigenclass 作为类的实例。完全与 subject 无关。必须自己手动将 subject 代入才行.. RSpec 我不知道呀。也没装~

你和 @lgn21st 两个人说了两个样,RSpec 也太绕了。

#26 楼 @zw963 确实是个没必要的 shortcut,显示指明 subject 要清晰很多

Google 到这里,各种神回复,感谢!

几个神回复下来,再也不用找其他资料了。

感谢@nightire的耐心解释和剖析。

Google 到这里,路过拾遗,感谢!

zhulinpinyu 关闭了讨论。 06月20日 08:52
zhulinpinyu 关闭了讨论。 06月20日 08:52
zhulinpinyu 重新开启了讨论。 06月20日 08:53
需要 登录 后方可回复, 如果你还没有账号请 注册新账号