rspec 中 let 和 subject 的区别

let 和 subject 很像,同出一源,都是通过委托来定义一个消息的接收方,这句话的意思可以理解成:为某个方法调用(此调用的结果是一个对象,这一点毋庸置疑,因为一切都是对象)绑定一个“名字”(一般用 symbol),于是在后面的测试样例中,我们可以用这个名字来指代它。最直接的好处就是可以让代码更精炼,提高可读性,减少重复。

说它们同出一源,可以通过源码获知:

def let(name, &block)
  ::RSpec::Core::MemoizedHelpers.module_for(self).define_method(name, &block)
  define_method(name) do
    __memoized.fetch(name) { |k| __memoized[k] = super() }
  end
end

简单地说,我们传递了 name 和 &block 给 let,于是返回给我们一个 defined method;再看 subject:

def subject(name=nil, &block)
  let(:subject, &block)
  alias_method name, :subject if name
end

see? subject 本身就是 let,只不过如果我们给了 name 的话,最后会把这个 name 作为最终结果的 alias_method

为什么要这么做呢?再来看一处源码便知:

def should(matcher=nil, message=nil)
  RSpec::Expectations::PositiveExpectationHandler.handle_matcher(subject, matcher, message)
end

原来,subject 是用来配合 should 进行隐式调用的,在这里何为隐式调用?举个例子:

# 不用 subject
describe "Checking Account initialization" do
  it "should have balance with $50" do
    account = CheckingAccount.new(Money.new(50, :USD))
    account.should have_a_balance_of(Money.new(50, :USD))  # should_have_a_balance 是自定义 matcher
  end
end

# 使用 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

如果你要使用主动式的 expectation,那么可以给 subject 起名字(非隐式调用):

describe "Checking Account initialization" do
  subject (:account) { CheckingAccount.new(Money.new(50, :USD)) }
  it "has $50 balance" do
    expect(account).to have_a_balance_of(Money.new(50, :USD))
  end
  it "has a balance attribute which equals the starting balance" do
    expect(account.balance).to eq(Money.new(50, :USD))
  end
end

上面的例子里,Money.new(50, :USD) 明显重复了很多次,但它又不是我们要测试的主题(subject 就是主题的意思),此时就是应该使用 let 的时候了:

# 重构上面的例子
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
This entry was posted in ruby on rails and tagged , , . Bookmark the permalink.

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

*

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>