26
Philly.rb meetup Rails testing: factories or fixtures? Michael Toppa March 11, 2014 @mtoppa

Rails testing: factories or fixtures?

  • Upload
    mtoppa

  • View
    1.525

  • Download
    2

Embed Size (px)

DESCRIPTION

A comparison of factories and fixtures for providing data in Rspec tests in Ruby on Rails, with a particular focus on FactoryGirl

Citation preview

Page 1: Rails testing: factories or fixtures?

Philly.rb meetupRails testing:factories or fixtures?

Michael ToppaMarch 11, 2014@mtoppa

Page 2: Rails testing: factories or fixtures?

Why use factories or fixtures?

❖ Factories and fixtures can simplify otherwise repetitive and complex data setup for tests

❖ Your need for them in unit tests will be light if you do TDD with loosely-coupled code

❖ But they are vital for unit-“ish” testing if you’re working with tightly coupled code

❖ e.g. most Rails apps, and the examples in this presentation

❖ They are great for integration testing

Page 3: Rails testing: factories or fixtures?

Comparisons

❖ Tests with no factories or fixtures

❖ Tests with fixtures

❖ Tests with factories, using FactoryGirl

❖ We’ll test the same method in each case, so you can clearly see the differences

Page 4: Rails testing: factories or fixtures?

Partial data model for our examples

CandidatCandidate e

(politician(politician))

RaceRaceCampaigCampaignn

OfficeOfficeCurrent Current HoldersHolders

Winning Campaign

Page 5: Rails testing: factories or fixtures?

Our test case

Page 6: Rails testing: factories or fixtures?

Tests with no factories or fixtures

Page 7: Rails testing: factories or fixtures?

Ok for simple cases# spec/models/candidate_spec.rb

describe Candidate do describe "#calculate_completeness" do it "returns 0 when no fields are filled out" do toppa = Candidate.new toppa.calculate_completeness.should eq(0.0) end

it "returns a ratio of filled-out fields to total fields" do toppa = Candidate.new toppa.name = "Mike Toppa" toppa.facebook_url = "https://facebook/ElJefe" toppa.wikipedia_url = "http://en.wikipedia.org/wiki/Mike_Toppa" toppa.calculate_completeness.should eq(0.2) # 3 / 15 = 0.2 endend

Page 8: Rails testing: factories or fixtures?

Not so good for complex cases

# spec/models/race_spec.rb

describe Race do describe '#inaugurate!' do it 'makes Mike Toppa President of the United States' do toppa = Candidate.create( name: 'Mike Toppa',

[and all required attributes])

president = Office.create(title: 'President of the United States',[and all required attributes]

) presidential_race = Race.create([president + all req attrs]) toppa_campaign = Campaign.create([toppa + presidential_race + all req attrs]) presidential_race.winning_campaign = toppa_campaign presidential_race.inaugurate! president.current_holders.first.should == toppa end endend

Page 9: Rails testing: factories or fixtures?

Tests with fixtures

Page 10: Rails testing: factories or fixtures?

The fixture files# spec/fixtures/candidates.ymlmike_toppa: id: 1 name: Mike Toppa gender: M [etc…]

# spec/fixtures/office.ymlpresident: id: 1 title: President of the United States level: N [etc…]

# spec/fixtures/race.ymlpresident_race_2012: id: 1 office: president election_day: 10/4/2012 [etc…]

# spec/fixtures/campaign.ymltoppa_us_president_campaign_2012: id: 1 race: president_race_2012 candidate: mike_toppa fec_id: XYZ [etc…]

Page 11: Rails testing: factories or fixtures?

The test file# spec/models/race_spec.rb

describe Race do

describe '#inaugurate!' do

it 'makes Mike Toppa President of the United States' do toppa = candidates(:mike_toppa) president = offices(:president) presidential_race = races(:us_president_race_2012) toppa_campaign = campaigns(:toppa_us_president_campaign_2012)

presidential_race.winning_campaign = toppa_campaign presidential_race.inaugurate! president.current_holders.first.should == toppa end endend

Page 12: Rails testing: factories or fixtures?

Pros

❖ For simple scenarios, re-usable across tests

❖ Easy to generate from live data

❖ Fixture files are easy to read

❖ Tests are fairly fast

❖ Records are inserted based on the fixture files, bypassing ActiveRecord

Page 13: Rails testing: factories or fixtures?

Cons

❖ Brittle

❖ If you add required fields to a model, you have to update all its fixtures

❖ Fixture files are an external dependency

❖ Fixtures are organized by model - you need to keep track of their relationships with test scenarios

❖ Not dynamic - you get the same values every time

❖ (this may or may not be ok)

Page 14: Rails testing: factories or fixtures?

Tests with factories,using FactoryGirl

Page 15: Rails testing: factories or fixtures?

The factory files# spec/factories/candidates.rbFactoryGirl.define do factory :candidate do name 'Jane Doe' gender 'F' [etc…]

# spec/factories/offices.rbFactoryGirl.define do factory :office do title 'Senator' level 'N' [etc…]

# spec/factories/races.rbFactoryGirl.define do factory :race do office election_day '11/04/2014'.to_datetime [etc…]

# spec/factories/campaigns.rbFactoryGirl.define do factory :campaign do candidate race fec_id 'XYZ' [etc…]

Page 16: Rails testing: factories or fixtures?

The test file# spec/models/race_spec.rb

describe Race do

describe '#inaugurate!' do

it 'makes Mike Toppa President of the United States' do toppa = create :candidate, name: 'Mike Toppa' president = create( :office, title: 'President of the United States' ) presidential_race = create :race, office: president toppa_campaign = create( :campaign, candidate: toppa, race: presidential_race ) presidential_race.winning_campaign = toppa_campaign presidential_race.inaugurate! president.current_holders.first.should == toppa end endend

Page 17: Rails testing: factories or fixtures?

But wait, there’s more…

Page 18: Rails testing: factories or fixtures?

Randomized values with the Faker gem

# spec/factories/candidates.rb

FactoryGirl.define do factory :candidate do name { Faker::Name.name } wikipedia_url { Faker::Internet.url } short_bio { Faker::Lorem.paragraph } phone { Faker::PhoneNumber.phone_number} address_1 { Faker::Address.street_address } address_2 { Faker::Address.secondary_address } city { Faker::Address.city } state { Faker::Address.state } zip { Faker::Address.zip_code } # and other kinds of randomized values gender { |n| %w[M F].sample } sequence(:pvs_id) azavea_updated_at { Time.at(rand * Time.now.to_i) }

[etc…]

Page 19: Rails testing: factories or fixtures?

Debate on randomized values

❖ Argument for:

❖ Having a wide variety of values, and combinations of values, in your tests can expose bugs you might otherwise miss

❖ Argument against:

❖ In a test using many different model instances, failures can be difficult to reproduce and debug

❖ If you’re counting on randomized values to find bugs, your design process may not be robust

Page 20: Rails testing: factories or fixtures?

Instantiation options

❖ create: saves your object to the database, and saves any associated objects to the database

❖ build: builds your object in memory only, but still saves any associated objects to the database

❖ build_stubbed: builds your object in memory only, as well as any associated objects

Page 21: Rails testing: factories or fixtures?

Instantiation options

❖ Use build_stubbed whenever possible - your tests will be faster!

❖ You will need to use create or build for integration testing

❖ …and if you’re stuck with tightly coupled Rails code

Page 22: Rails testing: factories or fixtures?

For frequent scenarios: child factories

# spec/factories/offices.rbFactoryGirl.define do factory :office do title 'Mayor' level 'L' [etc…]

factory :office_president do status 'A' title 'President of the United States' level 'N' [etc…]

end endend

# spec/models/race_spec.rbdescribe Race do describe '#inaugurate!' do it 'makes Mike Toppa President of the United States' do president = create :office_president [etc…] end endend

Page 23: Rails testing: factories or fixtures?

Child factories using other child factories

# spec/factories/offices.rb

FactoryGirl.define do factory :office do area title 'Mayor' level 'L' [etc…]

factory :office_house do association :area, factory: :congressional_district status 'A' level 'N' type_code 'H' [etc…] end endend

Page 24: Rails testing: factories or fixtures?

For frequent scenarios: traits

# spec/factories/candidates.rbFactoryGirl.define do factory :candidate do name 'Jane Doe' gender 'F' [etc…] end

trait :with_office_house do after :create do |candidate| office = create :office_house create :current_office_holder, :office => office, :candidate => candidate end endend

# spec/models/race_spec.rbdescribe Race do describe '#inaugurate!' do it 'makes Mike Toppa President of the United States' do toppa = create: candidate, :with_office_house [etc…] end endend

Page 25: Rails testing: factories or fixtures?

Don’t overuse child factories and traits - leads

to brittleness

Page 26: Rails testing: factories or fixtures?

Factories address shortcomings of fixtures

❖ Not as brittle

❖ Factory won’t break if you add a new required field to a model

❖ You don’t need to maintain complex scenarios spread out across fixture files

❖ Lessened external dependency, more flexibility

❖ You can define the attributes important to the test in the test code itself

❖ When using build_stubbed your tests will be faster

❖ But fixtures are faster when inserting records