Factories: Beyond ActiveRecord
Unlocking 10x productivity with factories

The humble Factory object
- Not a FactoryBot gem concept
- Not a Ruby on Rails concept
- Not a Ruby concept
- Dates back to the 80s and Smalltalk
- Made popular in the Design Patterns (GoF) book
FactoryBot misconceptions
- FactoryBot is only for creating ActiveRecord records
- FactoryBot records should be saved to the database
Building Ruby classes with FactoryBot
class Product
attr_reader :name, :price, :id
def initialize(id:, name:, price:)
@id = id
@name = name.to_s
@price = price.to_money
end
end
FactoryBot.define do
factory :product, class: 'Product' do
skip_create
id { Random.random_number }
name { 'iPod' }
price { '$299.99' }
initialize_with { new(id: id, name: name, price: price) }
end
end
Testing
test '#price returns a Money object' do
product = build(:product)
assert_instance_of Money, product.price
end
Benefits
- Cleaner tests
- Consistent object construction
Building External API Payloads / Responses In Tests
Problem
Static JSON fixtures rot quickly and don’t allow easy variation
Solution
Use factories to generate realistic data objects.
Factory methods tend to become large and difficult to grok
The problem
test '#call makes a request' do
stub_api_request(foo: 'bar', sometimes: false, maybe: 'no')
end
test '#call makes a request with fizz param' # TODO
private
def stub_api_request(foo: 'bar', sometimes: false, maybe: 'no', otherwise: 'yes')
{
# ...
}
end
Building External API Payloads (Hash/JSON)
factory :api_product_payload, class: Hash do
skip_create
transient do
product factory: :product
end
id { product.id }
name { product.name }
price { product.price }
timestamp { Time.now.iso8601 }
trait :pending do
status { "pending"}
end
initialize_with { attributes.except(:product).to_json }
end
More complex example
factory :api_product_response,
aliases: [:api_v1_product_response],
class: Hash do
skip_create
transient do
product factory: :product
end
# Core JSON:API top-level keys
initialize_with do
{
data: {
id: product.id.to_s,
type: 'product',
attributes: {
name: product.name,
price: product.price,
timestamp: Time.now.iso8601,
},
relationships: {},
},
meta: {},
}.to_json
end
end
Putting it all together
test '.create sends a POST to the /products endpoint with the payload' do
product = build(:product)
request_body = build(:api_product_payload, product: product)
response_body = build(:api_product_response, product: product)
stub_request(:post, "/products")
.with(body: request_body)
.to_return_json(body: response_body, status: 201)
described_class.create(product)
end
Generating common data types
FactoryBot.define do
factory(:PAN, aliases: [:card_number], class: 'String') do
skip_create
transient do
starting_number { '4' }
remaining_numbers { ('0'..'9').to_a.sample(15).join }
end
trait :visa # same as default
trait :mastercard do
starting_number { '5' }
end
initialize_with { "#{starting_number}#{remaining_numbers}" }
end
end
Generating value objects
class CleoBank::Transaction
def message_type
raw_transaction['MessageType']
end
def authorization?
message_type[1] == "1"
end
end
Generating value objects
class MessageType
def initialize(mti)
@mti = mti.to_s
end
def version
@mti[0]
end
def message_class
@mti[1]
end
def message_function
@mti[2]
end
def message_origin
@mti[3]
end
def authorization?
message_class == '0'
end
end
Generating value objects
class CleoBank::Transaction
def message_type
MessageType.new(raw_transaction['MessageType'])
end
delegate :authorization?, to: :message_type
end
FactoryBot.define do
factory(:ISO_8583_message_type, aliases: [:mti], class: 'MessageType') do
skip_create
version { '0' }
message_class { '2' }
message_function { '2' }
message_origin { '0' }
trait :authorization do
message_class { '0' }
end
# ...
initialize_with do
new([
version,
message_class,
message_function,
message_origin].join
)
end
end
end
In Summary
FactoryBot can be used to build any Ruby object
- Hashes, Arrays, Strings, etc.
- Classes (POROs)
- Value objects with many permutations
- Complex collections of stuff (not shown)
Why it matters
- Clearer intent
- Cleaner tests (Less duplication)
- More resilient tests
- Stronger domain modelling
Further reading
Factory bot book
https://thoughtbot.github.io/factory_bot/
Factories: Beyond ActiveRecord
By Gavin Morrice
Factories: Beyond ActiveRecord
- 6