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.