Upload
mtoppa
View
1.525
Download
2
Tags:
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
Philly.rb meetupRails testing:factories or fixtures?
Michael ToppaMarch 11, 2014@mtoppa
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
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
Partial data model for our examples
CandidatCandidate e
(politician(politician))
RaceRaceCampaigCampaignn
OfficeOfficeCurrent Current HoldersHolders
Winning Campaign
Our test case
Tests with no 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
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
Tests with 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…]
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
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
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)
Tests with factories,using FactoryGirl
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…]
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
But wait, there’s more…
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…]
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
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
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
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
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
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
Don’t overuse child factories and traits - leads
to brittleness
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