如何在rspec中测试ActiveRecord :: Relation对象的方法?

Bli*_*ing 10 activerecord rspec ruby-on-rails

如何测试仅适用于rspec中的ActiveRecord关系代理类的方法?例如sum,它看起来像什么@collection.sum(:attribute)

这是我正在尝试做的事情:

@invoice = stub_model(Invoice)
@line_item = stub_model(LineItem, {quantity: 1, cost: 10.00, invoice: @invoice})
@invoice.stub(:line_items).and_return([@line_item])

@invoice.line_items.sum(:cost).should eq(10)
Run Code Online (Sandbox Code Playgroud)

这不起作用,因为@invoice.line_items返回一个不像sumActiveRecord :: Relation对象那样定义的常规数组.

任何帮助是极大的赞赏.

Aar*_*n K 15

我不确定你在哪个Rails上,所以我将在这个例子中使用Rails 4.0.x; Rails 3.x的原则仍然适用.

TL; DR:你不想走这条路.

  • 考虑不存在模型规范
  • 考虑添加特定于域的API

你正在迅速走过嘲弄/顽固的道路.我一直走在这条路上,它并没有带来乐趣.部分原因归结为违反了得墨忒耳法则.部分原因是使用Rails API而不是创建自己的域API.

当您从ActiveRecord模型请求关系集合时,它不会返回Array您知道的内容.在Rails 4.0.x中,通过has_many关联,返回的类是:ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Model.

问题#1:错误的返回值

这里你的返回类型是Array.而实际的返回类型是ActiveRecord_Associations_CollectionProxy_Model.在存根/模拟土地中,这不一定是坏事.但是,如果您打算对存根返回的对象使用其他调用,则需要匹配相同的API协定.否则,你不会抄袭相同的行为.

在这种情况下,sumAR关联代理上定义的方法在运行时实际执行SQL.sum定义的方法Array通过Active Support修补.该Array#sum行为是根本不同的:

def sum(identity = 0, &block)
  if block_given?
    map(&block).sum(identity)
  else
    inject { |sum, element| sum + element } || identity
  end
end
Run Code Online (Sandbox Code Playgroud)

如您所见,它对元素求和,而不是所请求属性的总和.

问题#2:断言存根对象

你遇到的另一个主要问题是,你试图规定你的存根返回你所描述的内容.这没有意义.存根的要点是返回一个固定的答案.它不是断言它的行为方式.

你写的内容与以下内容没有根本区别:

invoice = stub_model(Invoice)
line_item = stub_model(LineItem, {quantity: 1, cost: 10.00, invoice: invoice})
invoice.stub(:line_items).and_return([line_item])

invoice.line_items.should eq([line_item])
Run Code Online (Sandbox Code Playgroud)

除非这应该是一个完整性检查,否则它对您的规格没有任何实际价值.

建议

我不确定你在这里写什么类型的规范.如果这是一个更传统的单元测试验收测试,那么我可能不会存在任何东西.有时候访问数据库并不一定有什么问题,尤其是当你测试的东西是你如何与它进行交互时; 这真的是你在这里做的.

您可以做的另一件事是开始使用它来创建您自己的特定域模型API.所有这些实际上意味着定义对您的域有意义的对象的接口,这些接口可能会也可能不会由DB或其他资源支持.

例如,拿你的invoice.line_items.sum(:cost).should eq(10),这显然是测试Rails AR API.在域名方面,它没有任何意义.但是,invoice.subtotal对您的域名可能意味着更多:

# app/models/invoice.rb
class Invoice < ActiveRecord::Base
  def subtotal
    line_items.sum(:cost)
  end
end

# spec/models/invoice_spec.rb
# These are unit specs on the model, which directly works with the DB
# it probably doesn't make sense to stub things here
describe Invoice do

  specify "the subtotal is the sum of all line item cost" do
    invoice = create(:invoice)
    3.times do |i|
      cost = (i + 1) * 2
      invoice.line_items.create(cost: cost)
    end

    expect(invoice.subtotal).to eq 12
  end

end
Run Code Online (Sandbox Code Playgroud)

现在稍后,当您Invoice在代码的其他部分中使用时,如果需要,可以轻松地将其存根:

# spec/helpers/invoice_helper_spec.rb
describe InvoiceHelper do

  context "requesting the formatted subtotal" do
    it "returns US dollars to two decimal places" do
      invoice = double(Invoice, subtotal: 1012)
      assign(:invoice, invoice)

      expect(helper.subtotal_in_dollars).to eq "$10.12"
    end
  end

end
Run Code Online (Sandbox Code Playgroud)

那么什么时候可以存根模型规格?嗯,这真的是一个判断调用,并且因人而异,代码库和代码库也各不相同.但是,仅仅因为某些东西app/models并不意味着它必须是ActiveRecord模型.在这些情况下,在协作者上存储域API可能很好.

编辑:createvsbuild

在上面的例子中我使用create(:invoice)invoice.line_items.create(cost: cost).但是,如果您担心数据库缓慢,您可能也很容易使用build(:invoice)invoice.line_items.build(cost: cost).

请注意,我用的create(:invoice),并build(:invoice)在这里是参照通用的"工厂",而不是一个特定的宝石的参考.你可以简单地使用Model.createModel.new代替他们.此外,line_items.createline_items.build由AR提供,并没有任何做任何工厂的宝石.