-
Zack Hobson
OpenSourcery Alumnus
Ruby on Rails is well known in its capacity as opinionated software, but there are a couple of things on which Rails and I never agreed. First among these is test fixtures, a feature that I learned to loathe immediately after I recognized the alternatives. My favored approach for creating test data is to generate it at testing time using a factory API, an idea I first saw described by Dan Manges a few years back. This idea has since then caught on, and now there are many implementations of what are most commonly termed fixture replacements.
The problems with fixtures are now fairly widely understood: they are difficult to maintain, they separate your test data from your tests, and they can increase the brittleness of your test suite. At the time, however, this idea hadn't caught on widely. Without many alternatives that met our needs, and inspired by Dan's post, Justin Balthrop and I collaborated on a package that directly implemented Dan's proposed API, called ModelFactory. Since then I've used this tool in a handful of projects as a replacement for fixtures, and Justin and I have expanded the functionality of ModelFactory as needs have arisen.
The purpose of ModelFactory is to generate valid ActiveRecord objects that can be used instead of fixture-generated objects. ModelFactory allows you to clearly demonstrate your intent because you only specify attribute values that you care about, while everything else is valid but essentially (and intentionally) opaque. Here's an example usage of the original ModelFactory API:
require 'model_factory' module Factory extend ModelFactory default User, { :name => 'Factory User', :login => 'factoryuser', :email => 'factoryuser@example.com', :active => true } end class SomeTest < Test::Unit::TestCase def test_inactive_user assert !Factory.create_user(:active => false).active? end end
Occasionally I'd check to see what new alternatives had become available. It was beginning to become clear to me that the ModelFactory API we'd adopted had some inherent limitations, mostly having to do with the creation of unique values and support for namespaced classes. Even still, I hadn't seen anything more appealing until I encountered machinist. Like ModelFactory, you use machinist by defining a set of default properties for your models:
require 'machinist/active_record' User.blueprint do name { "Factory User" } login { 'factoryuser' }, email { "#{login}@example.com" } active { true } end class SomeTest < Test::Unit::TestCase def test_inactive_user assert !User.make(:active => false).active? end end
One of the smartest features of machinist (and one that I borrowed for ModelFactory) is the use of blocks for assigning values. Since these blocks execute in the context of the new instance, it's possible to build values on top of one another. Machinist also includes an additional facility for generating unique values for your test data called Sham. This API can be combined with Faker to generate realistic-looking data for your tests:
require 'machinist/active_record' require 'sham' require 'faker' Sham.name { Faker::Name.name } Sham.email { Faker::Internet.email } User.blueprint do name email active { true } end
While a facility for generating unique values is obviously needed, I don't see the advantage of having random, realistic-looking test data. Luckily, the use of Faker is optional:
require 'machinist/active_record' require 'sham' Sham.name {|i| "Factory User #{i}" } Sham.login {|i| "factoryuser#{i}" } User.blueprint do name login email { "#{login}@example.com" } active { true } end
When used in this way, the Sham API seems like an unnecessary component. If all I want is a counter, why not just pass it in to the block generating the values? Here's a example of this technique as it appears in the latest ModelFactory:
require 'modelfactory' ModelFactory.configure do default(User) do name {|i| "Factory User #{i}" } login {|i| "factoryuser#{i}" } email { "#{login}@example.com" } active { true } end end
In this case the blocks are still being called in the context of the new instance, but they're also getting a counter passed in when the blocks have a single arity. This is accomplished using the excellent Ruby 1.9 method instance_exec, backported to earlier versions by ActiveSupport.
In this way I can have the best of both worlds while using less code. In order to improve support for namespaced classes, instances are created differently using the new ModelFactory API:
class SomeTest < Test::Unit::TestCase def test_inactive_user assert !User.factory.create(:active => false).active? end end
For a while I wasn't sure if I wanted to keep ModelFactory around when so many of its problems are solved by other packages. The combination of legacy compatibility and the features described above have granted ModelFactory a reprieve, at least until I find something that I'd rather be using instead.
It's important to evaluate your own solutions for continued relevance, and in the case of ModelFactory it took some work for me to get something that was worth keeping around, given the excellent alternatives. However, whether you use ModelFactory, machinist, Factory Girl or something else entirely, you'll be doing yourself a favor by avoiding stock Rails fixtures.
Tagged as: fixtures, ModelFactory, Ruby on Rails, testing