Tech

Rails Testing — Factory Girl

Factory Girl vs. Fixtures

What’s wrong with fixtures in the first place? First, go read my last blog post to see the pros and cons. There’s a decent list on the pros side, granted, but for many developers fixtures (or boilerplate setup in general) still sucks the life out of testing. Still not convinced that Factory Girl is even worth looking into? Let me google that for you.

That’s not to say that fixtures are useless – they have their place in many apps – but the bulk of your testing is greatly enhanced by using a factory.

Wait, what’s a factory? Here’s a short and sweet definition: a factory is an object whose sole job is to create other objects.

Why use Factory Girl?

The first (and possibly best) reason to use Factory Girl is because it solves the single worst problem of fixtures: maintenance. Tests become much easier to maintain when you can request a model instance that is always current. Using Factory Girl, a model is never bound to a particular phase of your application’s development. They are dynamically loaded from the current state of your application. Were there new Customer attributes introduced in that last merge? No problem, Factory Girl already sees them. Not so the case with a directory of fixtures.

Factory Girl provides an impressive set of features but today we’re going to focus on the basic creation/stub methods that are the bread and butter of testing.

Using Factory Girl’s Core Features

First off I’ll create an example, a crude one at that, of how a simple test might be written for a Customer model. It’ll just a be brief unit-test to verify that new customers have no last_payment value.


  # Create Customer instance - no factory, no fixture

  class SimpleCustomerTest < ActiveSupport::TestCase
    
    def setup
      @site = Site.create!(:name => "test site", :subdomain => "test-site")
      @company = Company.create!(site_id => @site.id, :name => "Spatula City, Inc.")
    end
    
    def test_new_customer_defaults
      c = Customer.create!(
        :company_id => @company.id, 
        :payment_plan => "basic", 
        :engagement => "monthly", 
        :subscription => true)
      
      assert_nil(c.last_payment, "Last payment should be nil")      
    end

  end

At first glance you might think hey, what’s the big deal? That wasn’t so tough. Creating a couple preparatory models (@site and @company) isn’t too hard but this is just one incredibly simple (read: useless) test. As your domain model grows the depth of related models grows in tandem. By the time you’re writing a test for say, a Customer’s refund, the volume of boilerplate code grows significantly.


  # Create all the models needed for a single refund test - no factory

  class SimpleCustomerTest < ActiveSupport::TestCase
    
    def setup
      @site = Site.create!(:name => "test site", :subdomain => "test-site")
      @company = Company.create!(:site_id => @site.id, :name => "Spatula City, Inc.")
      @customer = Customer.create!(
        :company_id => @company.id, 
        :payment_plan => "basic", 
        :engagement => "monthly", 
        :subscription => true)
      @product_abc = Product.create!(
        :company_id => @company.id, 
        :name => "The Zoom Spat", 
        :price => 11.0,
        :inventory => 20)
      @order = Order.create!(:customer_id => @customer.id)
      @line_item = @order.build_line_item(
        :product_id => @product_abc.id,
        :quantity => 1
      )
      @order.take_their_money!
    end
    
    def test_customer_refund
      # total charge simply tallies the sum of the line_items + shipping, etc.
      order_total = @order.total_charge

      # charge the customer
      @order.take_their_money!
    
      # reverse the charge, passing an amount to refund
      @order.refund_the_money!(order_total)
      refund_amount = @order.total_refund
      
      assert_equal(order_total, refund_amount, "We should have refunded the full amount of the order")      
    end
    
    ... lots more tests ...

  end

While more verbose than the first example, this code is still ridiculously simple. If your Customer/Order interactions involve discounts, product variations, or using third-party API’s, the girth of code required for setup quickly becomes tedious (read: painful).

Fixtures, you likely know, would look just like the code above except it would have numerous references to YAML files like so:


  @company   = YAML::load(File.open("test/fixtures/spatula_company.yaml", "rb").read)

I don’t see this as a big improvement. Granted, I can re-use that spatula_company.yaml file in numerous tests – that’s super – but what about when I need a list of new companies? Do I then copy my yaml files? Should I create a loop that iterates over the @company object and generates an array of new records? No thanks. That’s still brittle code that requires painful refactoring every time a change happens to my Company model.

Now let’s look at that same test written using Factory Girl…

Factory Girl’s Core Awesomeness

Before we can ask Factory Girl for a model we have to tell her how we expect them to be built. This is done in one or more factory definition files but we’ll just create one called factories.rb and put it here: /test/factories.rb.

Assembling the factory definitions for our previous example might look like so:


  # Let's just save all this as /test/factories
  FactoryGirl.define do
  
    factory :site do
      name      'test site'
      subdomain { "#{name}".gsub(/ /,'-') }
    end
    
    factory :company do
      site
      name 'Spatula City, Inc.'
      site_url 'test'
      pitch 'We sell spatulas.. and that's all!'
      city 'Spatville'
      state 'OR'
      country 'United States'
      status 'active'
    end

    factory :customer do
      company
      payment_plan 'basic'  
      engagement 'monthly'
      subscription true
    end
    
    sequence :product_name do |n|
      "The Zoom Spat #{n}"
    end
    
    factory :product do
      company
      name  { generate(:product_name) }
      price 11.0
      inventory 20
    end
    
    factory :line_item do
      product
      quantity 1
    end
    
    factory :order do
      customer

      ignore do
        total_items 1
      end

      after(:create) do |order, evaluator|
         FactoryGirl.create_list(:line_item, evaluator.total_items, order: order)
      end
    end
    
  end
  

To borrow directly from the Thoughtbot’s Github page here is example of what we can do with any of these factories:

Frankly, this blows the doors off using fixtures or creating these models manually. Behold the beauty, the simplicity, of Factory Girl in action.


  
  def test_customer_refund
    # this not only returns a new order but it also creates 5 line items
    @order = FactoryGirl.create(:order, :total_items => 5)
    
    # total charge simply tallies the sum of the line_items + shipping, etc.
    order_total = @order.total_charge

    # charge the customer
    @order.take_their_money!
    
    # reverse the charge, passing an amount to refund
    @order.refund_the_money!(order_total)
    refund_amount = @order.total_refund
      
    assert_equal(order_total, refund_amount, "We should have refunded the full amount of the order")      
  end

  ... lots more tests ...

end

Can you see how much easier it becomes to create tests to actually test your application vs. produce boilerplate garbage? Factory Girl’s ability to over-ride defined values by passing a hash makes your code significantly less brittle, and this is really just scratching the surface of what Factory Girl provides.

Even more awesomeness

  • Traits – I might just write a whole blog post about how cool these are
  • Lazy attributes – subdomain used this to evaluate the name
  • Sequences – I used one to generate new product names
  • Create multiple records at once!
  • You can change existing factories that you inherited from a gem – so they won’t suck so much!
  • You can utilize callbacks – did you see my after(:create) block in :order?

A response to the naysayers

  • Factories are slow.
    No, they’re really not.
    Still cranky? Buy an SSD. Get back to enjoying Ruby.
  • Factories increase the over-all complexity of your app.
    Are you kidding me? No, they do not. Factory Girl doesn’t require you become an expert before you make huge gains using the basics.

Which is where I leave you, at the basics. Please, start with the basics. Use more features only as you feel comfortable. Bite off what you can chew. The more time you spend using Factory Girl the easier it will become and your tests will a joy to maintain.

Thoughtbot, I salute you.

Written by

Seth is the Lead Developer at HiringThing, an online application provider dedicated to changing the way small and medium businesses hire talent. You can learn more about HiringThing at http://www.hiringthing.com.
blog comments powered by Disqus