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

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

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

共收到 30 条回复
1楼 已删除
1031

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

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

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

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

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

3

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

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

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

96

@nightire 厉害 :)

501

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

4552

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

4552

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

244

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

1031

#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 ?

3

#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
1031

#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 ??

96

@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 ?

96

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

1573

#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 对我们意味着什么,能让我们获得什么。

1573

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

96

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

3

#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 提供的。

1031

#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 ?

186

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

1031

#25楼 @doitian

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

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

186

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

4584

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

15615

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

96

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

43079a

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

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