Skip to content

Instantly share code, notes, and snippets.

@fjfish
Last active December 1, 2023 11:20
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fjfish/78bd55ffc708c16a400d to your computer and use it in GitHub Desktop.
Save fjfish/78bd55ffc708c16a400d to your computer and use it in GitHub Desktop.
Automatic generation of rspec model tests and factories for factory girl
class Models
def self.generate what = :model
Rails.application.eager_load!
ActiveRecord::Base.descendants.each do |model|
method(what)[model]
end
true
end
def self.factory model
factory_file_name = "spec/factories/#{model.name.underscore}.rb"
unless File.exists?(factory_file_name)
File.open(factory_file_name,"w") do |file|
factory_for model, file
end
end
end
def self.factory_for model, file
file << <<-EOT
FactoryBot.define do
factory :#{model.name.underscore}, :class => '#{model.name}' do
#{factory_cols model}
end
end
EOT
end
def self.factory_cols model
associations = model.reflections
"".tap do |output_text|
model.columns.each do |col|
next if col.name == 'id'
stripped_name = col.name.gsub(/_id$/,'').to_sym
output_text << "\n "
assoc = associations[stripped_name]
if assoc && [:has_one,:belongs_to].include?(assoc.macro)
output_text << if assoc.options[:class_name]
"association :#{stripped_name.to_s}, factory: :#{assoc.options[:class_name].underscore}"
else
stripped_name.to_s
end
else
output_text << "#{preprocess_name col.name, col.type } #{factory_default_for(col.name, col.type)}"
end
end
end
end
def self.preprocess_name name, type
case name
when /retry/
"self.#{name}"
when /e.*mail/,/name/
if [:text,:string].include?(type)
"sequence(:#{name})"
else
name
end
else
name
end
end
def self.factory_default_for name, type
case type
when :integer, :decimal
"1"
when :date
"Time.now.to_date"
when :datetime
"Time.now"
when :boolean
"true"
when :spatial
"nil"
else
case name
when /e.*mail/
'{ |n| "test#{n}@example.com" }'
when /name/
'{ |n| "name#{n}" }'
when /country/
'"GB"'
when /_ip$/
'"192.168.0.1"'
when /phone/
'"+44000000000"'
else
'"test123"'
end
end
end
def self.model model
test_file_name = "spec/models/#{model.name.underscore}_spec.rb"
unless File.exists?(test_file_name)
File.open(test_file_name,"w") do |file|
describe_model model, file
end
end
end
def self.describe_model model, file
file << <<-EOT
require 'rails_helper'
describe #{model.name}, :type => :model do
# let (:subject) { build :#{model.name.underscore} }
#{read_write_tests model}
#{associations model.reflections}
end
EOT
end
def self.read_write_tests model
" context \"validation\" do".tap do |output_text|
model.validators.select { |val| val.is_a? ActiveModel::Validations::PresenceValidator }.map(&:attributes).
flatten.each { |col| output_text << "\n it { should validate_presence_of :#{col} }"}
model.validators.select { |val| !val.is_a? ActiveModel::Validations::PresenceValidator }.
each { |col| output_text << "\n it \"#{col.class.to_s.demodulize.underscore} test for #{col.attributes.map(&:to_sym).to_s}\""}
end << "\n end"
end
def self.associations reflections
" context \"associations\" do".tap do |output_text|
reflections.each_pair { |key,assoc| output_text << "\n it { should #{translate_assoc assoc.macro} :#{test_assoc_name assoc} }"}
end << "\n end"
end
def self.translate_assoc macro
macro.to_s.gsub(/belongs/,'belong').gsub(/has/,'have')
end
def self.test_assoc_name assoc
case assoc.macro
when /have_many/
assoc.plural_name
when /has_one/,/belongs_to/
assoc.name
else
assoc.name
end
end
end
@fjfish
Copy link
Author

fjfish commented Nov 15, 2021

@jmscholen - thanks - amended.

Hope you found it useful.

IIRC my local copy had to put brackets around the factory stuff, but I haven't needed it for a few days so not checked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment