Posts for category: factories

More About Factories

This would be a great time to mention that if you have an questions to ask about Rails testing, put them in the comments or email them to railsprescriptions at gmail dot com and I’ll try to address them here.

First up: Felipe Coury, from the comments:

Would you mind on elaborating on the factory approach versus traditional approach of using fixtures, either on a new article or even replying to this comment with references you may have?

Sure—this will be covered in some detail in the book itself, but it’s also worth some attention here.

The goal of the factory approach is to work around two of the weaknesses of fixtures: brittleness and opacity. These problems generally show up when you try to use fixtures for something that requires a lot of data, like search or report functionality. The workflow pattern often is to create a bunch of fixtures, then write tests that validate that, say, searching for “Smith”, returns the correct two records.

The first problem comes when somebody needs to add a new record to the fixture set to expose a different case for a different test, but the new global set of fixtures breaks existing reporting tests. Eventually this becomes a real pain. Also, when you read the test that says that searching for Smith returns two records, you need to back up and check out the fixture file to verify that. In my experience, that leads to a lot of tests where you write the test, and just fill in the value that the program spits out, rather than determining the results before writing the test.

The alternate approach is to create separate data for each individual test. The goal of the various factory tools (Factory Girl, Fixture Replacement, and Machinist are three to look at) is to make creating data for each test easy enough to make it a viable option for complicated testing.

All three of these tools are similar enough for my purposes here—essentially you have a file where you define default templates for each ActiveRecord model in your system plus a factory method that creates a new instance based on the default values. The default values can be dynamic (Faker works nicely here to create random structured data), and you can override any value in the template with the actual value you need for the test.

The goal here is to get new objects with a minimum of typing and, more importantly, with the key values needed for the test highlighted and the values that are irrelevant to the test in the background. This can make the test much easier to read. The pattern is to create a minimum amount of data for each test, focused on exposing the specific issue under test. Here’s an example, using Machinist syntax.


  test "I have two doggies" do
    @doggie = Dog.make(:name => "Mr. Puddles")
    @other_doggie = Dog.make(:name => "Rex")
    assert_equal(2, Dog.all.size)
  end

The advantage is that you can create exactly the data you need for each test easily enough that it doesn’t seem like a burden. Each test is independent, and easy to read. If you do wind up needing the same setup in multiple tests, it’s easy enough to move the calls to a common method, or use one of the various context tools.

  context "with two doggies" do
    setup do
      @doggie = Dog.make(:name => "Mr. Puddles")
      @other_doggie = Dog.make(:name => "Rex")
    end

    test "the doggies can be friends" 
      @doggie.befriend(@other_doggie)
    end
  end

There are two downsides that I have encountered. One is speed—fixtures by default use database transactions to optimize loading, factories generally don’t. This can slow things down, but if you are using the factories to create only the minimum amount of data for each test, you should still be okay. Similarly, it’s easy for the factory setup data to get so complex that it winds up in it’s own method, then it’s own module, which can take you close to the hard-to-read nature of fixture tests. I still think the factory tests are better, because you can group data from different models in the same file, but it’s something to watch out for.

Hope that helps, let me know if there are other questions you want me to cover here.