Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practice for FactoryGirl with deep association chains?

I am modeling a complex purchasing workflow in Rails that converts Requisitions to Orders. I'm using FactoryGirl to do my testing and all is well, until I try to test the OrderLineItem, which depends on an Order and a Quote, which each depend on other objects, and so on...

The test in question checks behavior on the OrderLineItem that is affected by the Product, which is several associations higher up the chain.

Is there a good way to setup FactoryGirl so I can easily build OrderLineItems and also dictate the behavior of the objects higher up in the chain without factoring each object one at a time?

Here's my object graph:

class Requisition
  has_many :requisition_line_items
  has_many :orders
end

class RequisitionLineItem
  belongs_to :requisition
  belongs_to :product
  has_many :quotes
end

class Quote
  belongs_to :line_item
  belongs_to :vendor
  has_one :order_line_item
end

class Order
  belongs_to :requisition
  belongs_to :vendor
  has_many :order_line_items
end

class OrderLineItem
  belongs_to :order
  belongs_to :quote
  has_many :assets
end

class Asset
  belongs_to :order_line_item
  belongs_to :product
end

class Product
  has_many :assets
end

class Vendor
  has_many :orders
end

The seemingly complex model allows a purchase "proposal" to be converted into one or more actual orders based on quotes from vendors, and when the items arrive, they are given asset tags. The assets themselves can then be linked back to an order and a vendor for support later on.

It my OrderLineItem spec, I have a a rather terse setup:

describe '#requires_tag?' do

  let(:product)              { FactoryGirl.create :product, requires_tag: false }
  let(:purchase_requisition) { FactoryGirl.create :purchase_requisition }
  let(:line_item)            { FactoryGirl.create :line_item, 
                                 purchase_requisition: purchase_requisition, 
                                 product: product }
  let(:quote)                { FactoryGirl.create :quote, 
                                 line_item: line_item, unit_price: 0 }

  subject { FactoryGirl.build :order_line_item, quote: quote }

  context 'when neither product nor price require a tag' do
    its(:requires_tag?) { should be_false }
  end

  context 'when product requires a tag' do
    let(:product) { FactoryGirl.create :product, requires_tag: true }
    its(:requires_tag?) { should be_true }
  end

end

Do I really need the myriad let statements, or is there a better way to build an OrderLineItem and exert control over the attributes of the Product it depends on?

like image 976
Jarrod Carlson Avatar asked Oct 05 '22 04:10

Jarrod Carlson


2 Answers

I have to disagree w/ cpuguy. I agree that the law of demeter is a great thing, but your object graph only seems to be violating it due to the impedence mismatch between your relational database and the hierarchical data you're storing.

If there's something here that could be refactored, it might be your model structure, or your model storage mechanism. The Demeter issue you're having is a symptom of the fact that you're using a relational system to model a hierarchical data model. Consider whether all of the order info was just in a big hash. I don't think you'd feel the same level of pain. The only alternative to that would be to try and copy some of those fields down to where you're using them.

I actually think your specs are excellent, because: a) They're behavioral, testing what sounds like discrete elements of business functionality that aren't likely to change b) If your specs require mocking out internals, the specs become useless in terms of refactoring down the road, because your expectations have to change w/ them.

The main issue is just in how to construct your test environments. You can abstract those out into a higher-level factory, but be careful that you don't wind up hiding what makes your spec unique. You've done a good job of that though, too. The one recommendation I might suggest, would be to create a let(:requires_tag) in each context, one with it true, and one with it false. Then leave everything else in your setup. That way it's clear how each context varies from the main setup, which can take a bit longer to grok.

Aside from that, if there's a better way to do this, I haven't found it.

like image 103
Jeff D Avatar answered Oct 06 '22 17:10

Jeff D


I would say try to refactor this so you are testing one thing. If you are doing that you don't need to create all these objects (and in turn slow down your test).

If you are not violating LoD, https://www.google.com/search?q=law+of+demeter&ie=UTF-8&oe=UTF-8&hl=en&client=safari#itp=open0, then this becomes a lot easier.

You should be able to stub the methods you are calling on the related objects as needed instead of creating the actual relationships.

like image 33
cpuguy83 Avatar answered Oct 06 '22 18:10

cpuguy83