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

时间:2014-05-08 08:21:52

标签: ruby-on-rails activerecord rspec

如何测试仅适用于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)

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

非常感谢任何帮助。

1 个答案:

答案 0 :(得分:14)

我不确定您使用的是哪个Rails,因此我将在此示例中使用Rails 4.0.x; Rails 3.x的原则仍然适用。

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

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

你正在迅速走向嘲弄/顽固的道路。我一直走在这条路上,它并没有带来乐趣。部分原因归结为违反Law of Demeter。部分原因在于使用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协定。否则,您不会抄袭相同的行为。

在这种情况下,AR关联代理上定义的sum方法在运行时实际执行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

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

问题#2:断言存根' d对象

您遇到的另一个主要问题是,您正在尝试规定您的存根会返回您存根的内容。这没有意义。存根的要点是返回一个固定的答案。它没有断言它的行为方式。

你所写的内容与以下内容并无根本不同:

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])

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

建议

我不确定你在这里写什么类型的规范。如果这是一个更传统的单元测试验收测试,那么我可能不会存在任何东西。有时候点击数据库并不一定有什么问题,特别是当你测试的东西是你与它的互动方式时;这就是你在这里做的事。

您可以做的另一件事是开始使用它来创建您自己的特定域模型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

现在稍后,当您在代码的其他部分使用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

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

编辑:create vs build

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

请注意,我在这里使用create(:invoice)build(:invoice)是指通用&#34;工厂&#34;,而不是对特定gem的引用。您只需在其位置使用Model.createModel.new即可。此外,line_items.createline_items.build由AR提供,与任何工厂宝石无关。