2. 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
3. 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
4. Partial data model for our
examples
CandidateCandidate
(politician)(politician)
RaceRaceCampaignCampaign
OfficeOffice
CurrentCurrent
HoldersHolders
Winning Campaign
7. 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
end
end
8. 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
end
end
10. The fixture files
# spec/fixtures/candidates.yml
mike_toppa:
id: 1
name: Mike Toppa
gender: M
[etc…]
# spec/fixtures/office.yml
president:
id: 1
title: President of the United States
level: N
[etc…]
# spec/fixtures/race.yml
president_race_2012:
id: 1
office: president
election_day: 10/4/2012
[etc…]
# spec/fixtures/campaign.yml
toppa_us_president_campaign_2012:
id: 1
race: president_race_2012
candidate: mike_toppa
fec_id: XYZ
11. 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
end
end
12. 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
13. 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)
15. The factory files
# spec/factories/candidates.rb
FactoryGirl.define do
factory :candidate do
name 'Jane Doe'
gender 'F'
[etc…]
# spec/factories/offices.rb
FactoryGirl.define do
factory :office do
title 'Senator'
level 'N'
[etc…]
# spec/factories/races.rb
FactoryGirl.define do
factory :race do
office
election_day '11/04/2014'.to_datetime
[etc…]
# spec/factories/campaigns.rb
FactoryGirl.define do
factory :campaign do
candidate
race
fec_id 'XYZ'
16. 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
end
end
18. 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…]
19. 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
20. 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
21. 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
22. For frequent scenarios: child
factories
# spec/factories/offices.rb
FactoryGirl.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
end
end
# spec/models/race_spec.rb
describe Race do
describe '#inaugurate!' do
it 'makes Mike Toppa President of the United States' do
president = create :office_president
[etc…]
end
end
end
23. 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
end
end
24. For frequent scenarios: traits
# spec/factories/candidates.rb
FactoryGirl.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
end
end
# spec/models/race_spec.rb
describe Race do
describe '#inaugurate!' do
it 'makes Mike Toppa President of the United States' do
toppa = create: candidate, :with_office_house
[etc…]
end
end
end
26. 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
Editor's Notes
Introductions to using tools usually have overly-simple examples. We’re going to use more realistic examples, as you can only get a feel for the differences between factories and fixtures when dealing with more complex scenarios.
The method we’ll examine is for inaugurating a new office holder, and we’ll make me President of the United States
Fast, simple, clear (not hitting the database)
Specifying all the attributes is cumbersome
and when reading the test, distracts from understanding the purpose of the test
Brittle: if add a required field to a model, you have to update all the tests using that model
Hard to reuse
For each model’s fixture file, you can have multiple records defined
updated_at and created_at are set automatically with the current time, or you can specify them
A simple factory file can look a lot like a fixture file…
… but a key difference is that you can override values and specify associations when writing the test