From Fixtures to Factories «
»


Code: , , , , , ,
3 comments

Automated tests need example data, and it’s a pain to have to construct a complete object in every test, especially when there are a lot of non-optional fields.

The standard improvement is to use fixtures, a file with some example data, that your tests can load by giving the name of a fixture. Here’s one of the fixtures for a gamer:

alice: # alice is heavily involved
  id: 1
  mail: alice@example.com
  handle: alice
  password: red queen
  teaser: Follow the white rabbit.
  profile: Not so plain, after all.
  homepage: http://www.example.com
  jabber: alice@example.org
  location_id: 3
  last_login: 2006-10-30 11:56:08
  created_at: 2006-10-30 11:56:08
  location_at: 2006-10-30 11:56:08
  tag_text: dnd, wod
  mail_on_message: true
  mail_on_nearby_discussion: true

This is straightforward, but location_id is the id of another fixture in another file. As you can imagine it’s pretty easy to get them out-of-sync and break some tests. I end up leaving little comments in the YAML to remind myself of the cross-reference.

It’s awfully tempting to reuse fixtures from test to test. Maybe Alice was written to test logging in with a username and password and next I’m writing the test of logging in with an email address and a password. I could copy and paste Alice to another fixture to reuse in that test, but as a programmer I have a pathological aversion to copy and paste. Alice is going to get reused (most of those fields are optional and were used by very different tests), and after this happens a few times it’s hard to change a fixture without breaking an unrelated test.

I’ve gotten a lot of use out of fixtures as I’ve developed the habit of testing all my code, but reuse has made my tests somewhat brittle. After a bit of research, I’ve moved over to using factories.

Here’s the factory for a gamer:

Factory.sequence :handle { |n| "Gamer_#{n}" }
 
Factory.define :gamer do |g|
  g.handle { Factory.next :handle }
  g.mail { |m| "#{m.handle}@example.com" }
  g.password 'secret'
end

Only the essential, required fields are listed. Using this simple template, the tests themselves specify the values of the fields they’ll be testing.

def test_login_with_valid_username_and_password
  gamer = Gamer.create :handle => 'Alice', :password => 'red queen'
  ...
end
def test_login_with_valid_email_and_password
  gamer = Gamer.create :mail => 'alice@example.com', :password => 'red queen'
  ...
end

Ideally, changing a fixture wouldn’t break any tests at all. And factory_girl (what I’m using for factories in Rails) makes associations easy:

Factory.define :post do |p|
  p.association :discussion, :factory => :discussion
  p.association :poster, :factory => :gamer
  p.created_at Time.now.utc
  p.body "Post Body"
end

Now my tests include the specific data they care about, making them easier to understand and improve. And they don’t interrelate, so they’re much more reliable. I do still use fixtures, but now they’re exclusively for tests that need to deal with real-world examples.


Comments

  1. I recently learned that you can also follow associations in your fixtures to avoid fixtures getting out of sync once the associations are declared in the models. This is really handy for has_many fixtures, because you don’t need a join fixture (This is a tortured example, I know)

    locations.yml
    through_the_looking_glass:
       name: Through the looking glass
     
    gamers.yml
    alice:
      jabber: alice@example.org
      location: through_the_looking_glass
      subjects: knave_hearts, eight_hearts, four_hearts
     
    subjects.yml
    knave_hearts:
     suit: Hearts
     number: J
    
    eight_hearts:
     suit: Hearts
     number: 8
    
    four_hearts:
     suit: Hearts
     number: 4
  2. Yeah, named associations like that are a step up from id numbers and comments (I forgot about them, I switched this code over before they were available in Rails), but it’s still easy to get them out-of-sync.

  3. This post reminds me of mutation testing. Ruby implementations include Heckle, Boo_hiss, chaser, and zombie-chaser.

Leave a Reply

Your email address will not be published.