From 61860f06e72ac3958817bd8b61a0151bb98c4f46 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Wed, 17 Feb 2021 11:10:17 +0000 Subject: [PATCH 1/5] Implement a rake task to generate fake data for development purposes It can be quite time consuming to add data, such as diary entries, notes, and users to your development database. This is a basic implementation of an idempotent rake task that does some of the hard work for you. --- Gemfile | 7 ++++- Gemfile.lock | 5 +++ .../factory_bot_strategy_find_or_create.rb | 31 +++++++++++++++++++ lib/tasks/development.rake | 14 +++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 config/initializers/factory_bot_strategy_find_or_create.rb create mode 100644 lib/tasks/development.rake diff --git a/Gemfile b/Gemfile index a5b7d401f48..aec73f91827 100644 --- a/Gemfile +++ b/Gemfile @@ -125,16 +125,21 @@ group :development do gem "better_errors" gem "binding_of_caller" gem "debug_inspector" + gem "faker" gem "listen" + gem "timecop" gem "vendorer" end +group :development, :test do + gem "factory_bot_rails" +end + # Gems needed for running tests group :test do gem "brakeman" gem "capybara", ">= 2.15" gem "erb_lint", :require => false - gem "factory_bot_rails" gem "minitest", "~> 5.1" gem "puma", "~> 5.0" gem "rails-controller-testing" diff --git a/Gemfile.lock b/Gemfile.lock index 31d64b6b8c7..eb923944d76 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -197,6 +197,8 @@ GEM factory_bot_rails (6.1.0) factory_bot (~> 6.1.0) railties (>= 5.0.0) + faker (2.16.0) + i18n (>= 1.6, < 2) faraday (1.4.1) faraday-excon (~> 1.1) faraday-net_http (~> 1.0) @@ -448,6 +450,7 @@ GEM thor (1.1.0) thread_safe (0.3.6) tilt (2.0.10) + timecop (0.9.4) tzinfo (1.2.9) thread_safe (~> 0.1) uglifier (4.2.0) @@ -496,6 +499,7 @@ DEPENDENCIES delayed_job_active_record erb_lint factory_bot_rails + faker faraday ffi-libarchive gd2-ffij (>= 0.4.0) @@ -547,6 +551,7 @@ DEPENDENCIES simplecov simplecov-lcov strong_migrations + timecop uglifier (>= 1.3.0) validates_email_format_of (>= 1.5.1) vendorer diff --git a/config/initializers/factory_bot_strategy_find_or_create.rb b/config/initializers/factory_bot_strategy_find_or_create.rb new file mode 100644 index 00000000000..4a63457e538 --- /dev/null +++ b/config/initializers/factory_bot_strategy_find_or_create.rb @@ -0,0 +1,31 @@ +# From https://stackoverflow.com/a/65700370/ +module FactoryBot + module Strategy + # Does not work when passing objects as associations: `FactoryBot.find_or_create(:entity, association: object)` + # Instead do: `FactoryBot.find_or_create(:entity, association_id: id)` + class FindOrCreate + def initialize + @build_strategy = FactoryBot.strategy_by_name(:build).new + end + + delegate :association, :to => :@build_strategy + + def result(evaluation) + attributes = attributes_shared_with_build_result(evaluation) + evaluation.object.class.where(attributes).first || FactoryBot.strategy_by_name(:create).new.result(evaluation) + end + + private + + # Here we handle possible mismatches between initially provided attributes and actual model attrbiutes + # For example, devise's User model is given a `password` and generates an `encrypted_password` + # In this case, we shouldn't use `password` in the `where` clause + def attributes_shared_with_build_result(evaluation) + object_attributes = evaluation.object.attributes + evaluation.hash.filter { |k, _v| object_attributes.key?(k.to_s) } + end + end + end + + register_strategy(:find_or_create, Strategy::FindOrCreate) +end diff --git a/lib/tasks/development.rake b/lib/tasks/development.rake new file mode 100644 index 00000000000..bb9d1bfb235 --- /dev/null +++ b/lib/tasks/development.rake @@ -0,0 +1,14 @@ +if Rails.env.development? + desc "Populate the development database with some fake data" + namespace "dev" do + task "populate" => :environment do + # Ensure we generate the same data each time this is run + Faker::Config.random = Random.new(42) + + # Ensure that all dates (e.g. terms_agreed) are consistent + Timecop.freeze(Time.utc(2015, 10, 21, 12, 0, 0)) do + _user = FactoryBot.find_or_create(:user, :display_name => Faker::Name.name) + end + end + end +end From 4d817ff377d7355c472db4d98eb1cc3f80a8b421 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Wed, 17 Feb 2021 12:55:45 +0000 Subject: [PATCH 2/5] Add a diary entry, as an example --- lib/tasks/development.rake | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/tasks/development.rake b/lib/tasks/development.rake index bb9d1bfb235..5befe6b523a 100644 --- a/lib/tasks/development.rake +++ b/lib/tasks/development.rake @@ -7,7 +7,11 @@ if Rails.env.development? # Ensure that all dates (e.g. terms_agreed) are consistent Timecop.freeze(Time.utc(2015, 10, 21, 12, 0, 0)) do - _user = FactoryBot.find_or_create(:user, :display_name => Faker::Name.name) + user = FactoryBot.find_or_create(:user, :display_name => Faker::Name.name) + + FactoryBot.find_or_create(:diary_entry, :title => Faker::Lorem.sentence, + :body => Array.new(3) { Faker::Lorem.paragraph(:sentence_count => 12) }.join("\n\n"), + :user_id => user.id) end end end From e79cef894da4a4132e7e419fb1062ceb72c2a704 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Wed, 17 Feb 2021 12:57:43 +0000 Subject: [PATCH 3/5] Include factorybot syntax, for less typing --- lib/tasks/development.rake | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/tasks/development.rake b/lib/tasks/development.rake index 5befe6b523a..70ea781e101 100644 --- a/lib/tasks/development.rake +++ b/lib/tasks/development.rake @@ -2,16 +2,18 @@ if Rails.env.development? desc "Populate the development database with some fake data" namespace "dev" do task "populate" => :environment do + include FactoryBot::Syntax::Methods + # Ensure we generate the same data each time this is run Faker::Config.random = Random.new(42) # Ensure that all dates (e.g. terms_agreed) are consistent Timecop.freeze(Time.utc(2015, 10, 21, 12, 0, 0)) do - user = FactoryBot.find_or_create(:user, :display_name => Faker::Name.name) + user = find_or_create(:user, :display_name => Faker::Name.name) - FactoryBot.find_or_create(:diary_entry, :title => Faker::Lorem.sentence, - :body => Array.new(3) { Faker::Lorem.paragraph(:sentence_count => 12) }.join("\n\n"), - :user_id => user.id) + find_or_create(:diary_entry, :title => Faker::Lorem.sentence, + :body => Array.new(3) { Faker::Lorem.paragraph(:sentence_count => 12) }.join("\n\n"), + :user_id => user.id) end end end From e944d1e72bc3cf9ae9fdeb7735e68e5023ef7a99 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Wed, 17 Feb 2021 14:34:42 +0000 Subject: [PATCH 4/5] Ensure the find_or_create strategy is only available in the development environment We don't want any FactoryBot stuff available in production, and this shouldn't be used by mistake in the test environment either. --- .../factory_bot_strategy_find_or_create.rb | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/config/initializers/factory_bot_strategy_find_or_create.rb b/config/initializers/factory_bot_strategy_find_or_create.rb index 4a63457e538..0e9e7cdf092 100644 --- a/config/initializers/factory_bot_strategy_find_or_create.rb +++ b/config/initializers/factory_bot_strategy_find_or_create.rb @@ -1,31 +1,33 @@ # From https://stackoverflow.com/a/65700370/ -module FactoryBot - module Strategy - # Does not work when passing objects as associations: `FactoryBot.find_or_create(:entity, association: object)` - # Instead do: `FactoryBot.find_or_create(:entity, association_id: id)` - class FindOrCreate - def initialize - @build_strategy = FactoryBot.strategy_by_name(:build).new - end +if Rails.env.development? + module FactoryBot + module Strategy + # Does not work when passing objects as associations: `FactoryBot.find_or_create(:entity, association: object)` + # Instead do: `FactoryBot.find_or_create(:entity, association_id: id)` + class FindOrCreate + def initialize + @build_strategy = FactoryBot.strategy_by_name(:build).new + end - delegate :association, :to => :@build_strategy + delegate :association, :to => :@build_strategy - def result(evaluation) - attributes = attributes_shared_with_build_result(evaluation) - evaluation.object.class.where(attributes).first || FactoryBot.strategy_by_name(:create).new.result(evaluation) - end + def result(evaluation) + attributes = attributes_shared_with_build_result(evaluation) + evaluation.object.class.where(attributes).first || FactoryBot.strategy_by_name(:create).new.result(evaluation) + end - private + private - # Here we handle possible mismatches between initially provided attributes and actual model attrbiutes - # For example, devise's User model is given a `password` and generates an `encrypted_password` - # In this case, we shouldn't use `password` in the `where` clause - def attributes_shared_with_build_result(evaluation) - object_attributes = evaluation.object.attributes - evaluation.hash.filter { |k, _v| object_attributes.key?(k.to_s) } + # Here we handle possible mismatches between initially provided attributes and actual model attrbiutes + # For example, devise's User model is given a `password` and generates an `encrypted_password` + # In this case, we shouldn't use `password` in the `where` clause + def attributes_shared_with_build_result(evaluation) + object_attributes = evaluation.object.attributes + evaluation.hash.filter { |k, _v| object_attributes.key?(k.to_s) } + end end end - end - register_strategy(:find_or_create, Strategy::FindOrCreate) + register_strategy(:find_or_create, Strategy::FindOrCreate) + end end From 37ac6b8074cdfc2b38c47a828f2b4db05f234229 Mon Sep 17 00:00:00 2001 From: Andy Allan Date: Wed, 17 Feb 2021 14:40:48 +0000 Subject: [PATCH 5/5] Add some more detailed comments --- lib/tasks/development.rake | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/tasks/development.rake b/lib/tasks/development.rake index 70ea781e101..152599d335f 100644 --- a/lib/tasks/development.rake +++ b/lib/tasks/development.rake @@ -1,3 +1,11 @@ +# Create some fake data to help developers get started with the app + +# This rake task will add some fake data, which is particularly helpful when +# developing the user interface, since it avoids time-consuming manual work + +# The data should be created in a idempotent fashion, so that the script can be +# run again without causing errors or duplication. + if Rails.env.development? desc "Populate the development database with some fake data" namespace "dev" do