From ed5e4a6c7a7913a331e4bce30ddfa96626798d51 Mon Sep 17 00:00:00 2001 From: Steve Polito Date: Wed, 18 Oct 2023 07:48:48 -0400 Subject: [PATCH] Introduce `suspenders:factory_bot` generator Maintains functionally with the [existing generator][] while adding support for the [default Rails test suite]. With this change, the generator can be invoked on a Rails application that use RSpec and the default Rails test suite. Adds generator which adds a test to lint all Factories in an effort to improve developer experience. [existing generator]: https://github.com/thoughtbot/suspenders/blob/main/lib/suspenders/generators/factories_generator.rb [default Rails test suite]: https://guides.rubyonrails.org/testing.html --- .../suspenders/factory_bot_generator.rb | 70 ++++++ lib/generators/templates/factory_bot/dev.rake | 12 + .../templates/factory_bot/factories.rb | 2 + .../templates/factory_bot/factories_test.rb | 9 + .../factory_bot/factory_bot_rspec.rb | 5 + .../suspenders/factory_bot_generator_test.rb | 215 ++++++++++++++++++ 6 files changed, 313 insertions(+) create mode 100644 lib/generators/suspenders/factory_bot_generator.rb create mode 100644 lib/generators/templates/factory_bot/dev.rake create mode 100644 lib/generators/templates/factory_bot/factories.rb create mode 100644 lib/generators/templates/factory_bot/factories_test.rb create mode 100644 lib/generators/templates/factory_bot/factory_bot_rspec.rb create mode 100644 test/generators/suspenders/factory_bot_generator_test.rb diff --git a/lib/generators/suspenders/factory_bot_generator.rb b/lib/generators/suspenders/factory_bot_generator.rb new file mode 100644 index 000000000..8dc9b24b0 --- /dev/null +++ b/lib/generators/suspenders/factory_bot_generator.rb @@ -0,0 +1,70 @@ +module Suspenders + module Generators + class FactoryBotGenerator < Rails::Generators::Base + source_root File.expand_path("../../templates/factory_bot", __FILE__) + + def add_factory_bot + gem_group :development, :test do + gem "factory_bot_rails" + end + + Bundler.with_unbundled_env { run "bundle install" } + end + + def set_up_factory_bot + if default_test_helper_present? + insert_into_file Rails.root.join("test/test_helper.rb"), after: "class TestCase" do + "\n include FactoryBot::Syntax::Methods" + end + elsif rspec_test_helper_present? + copy_file "factory_bot_rspec.rb", "spec/support/factory_bot.rb" + insert_into_file Rails.root.join("spec/rails_helper.rb") do + %(Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file }) + end + end + end + + def generate_empty_factories_file + if default_test_suite? + copy_file "factories.rb", "test/factories.rb" + elsif rspec_test_suite? + copy_file "factories.rb", "spec/factories.rb" + end + end + + def provide_dev_prime_task + copy_file "dev.rake", "lib/tasks/dev.rake" + end + + def remove_fixture_definitions + if default_test_helper_present? + comment_lines "test/test_helper.rb", /fixtures :all/ + end + end + + def create_linting_test + if default_test_suite? + copy_file "factories_test.rb", "test/factory_bots/factories_test.rb" + end + end + + private + + def default_test_suite? + File.exist? Rails.root.join("test") + end + + def rspec_test_suite? + File.exist? Rails.root.join("spec") + end + + def default_test_helper_present? + File.exist? Rails.root.join("test/test_helper.rb") + end + + def rspec_test_helper_present? + File.exist? Rails.root.join("spec/rails_helper.rb") + end + end + end +end diff --git a/lib/generators/templates/factory_bot/dev.rake b/lib/generators/templates/factory_bot/dev.rake new file mode 100644 index 000000000..7a94175f1 --- /dev/null +++ b/lib/generators/templates/factory_bot/dev.rake @@ -0,0 +1,12 @@ +if Rails.env.development? || Rails.env.test? + require "factory_bot" + + namespace :dev do + desc "Sample data for local development environment" + task prime: "db:setup" do + include FactoryBot::Syntax::Methods + + # create(:user, email: "user@example.com", password: "password") + end + end +end diff --git a/lib/generators/templates/factory_bot/factories.rb b/lib/generators/templates/factory_bot/factories.rb new file mode 100644 index 000000000..3bfcbd203 --- /dev/null +++ b/lib/generators/templates/factory_bot/factories.rb @@ -0,0 +1,2 @@ +FactoryBot.define do +end diff --git a/lib/generators/templates/factory_bot/factories_test.rb b/lib/generators/templates/factory_bot/factories_test.rb new file mode 100644 index 000000000..23b78663d --- /dev/null +++ b/lib/generators/templates/factory_bot/factories_test.rb @@ -0,0 +1,9 @@ +require "test_helper" + +class FactoryBotsTest < ActiveSupport::TestCase + class FactoryLintingTest < FactoryBotsTest + test "linting of factories" do + FactoryBot.lint traits: true + end + end +end diff --git a/lib/generators/templates/factory_bot/factory_bot_rspec.rb b/lib/generators/templates/factory_bot/factory_bot_rspec.rb new file mode 100644 index 000000000..4943f172e --- /dev/null +++ b/lib/generators/templates/factory_bot/factory_bot_rspec.rb @@ -0,0 +1,5 @@ +FactoryBot.use_parent_strategy = true + +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end diff --git a/test/generators/suspenders/factory_bot_generator_test.rb b/test/generators/suspenders/factory_bot_generator_test.rb new file mode 100644 index 000000000..661301016 --- /dev/null +++ b/test/generators/suspenders/factory_bot_generator_test.rb @@ -0,0 +1,215 @@ +require "test_helper" +require "generators/suspenders/factory_bot_generator" + +module Suspenders + module Generators + class FactoryBotGenerator::DefaultTest < Rails::Generators::TestCase + include Suspenders::TestHelpers + + tests Suspenders::Generators::FactoryBotGenerator + destination Rails.root + setup :prepare_destination + teardown :restore_destination + + test "generator runs without errors" do + assert_nothing_raised do + run_generator + end + end + + test "installs gem with Bundler" do + Bundler.stubs(:with_unbundled_env).yields + generator.expects(:run).with("bundle install").once + + capture(:stdout) do + generator.add_factory_bot + end + end + + test "removes fixture definitions" do + File.open(app_root("test/test_helper.rb"), "w") { _1.write test_helper } + + run_generator + + assert_file app_root("test/test_helper.rb") do |file| + assert_match(/# fixtures :all/, file) + end + end + + test "adds gem to Gemfile" do + run_generator + + assert_file app_root("Gemfile") do |file| + assert_match(/group :development, :test do\n gem "factory_bot_rails"\nend/, file) + end + end + + test "includes syntax methods" do + File.open(app_root("test/test_helper.rb"), "w") { _1.write test_helper } + + run_generator + + assert_file app_root("test/test_helper.rb") do |file| + assert_match(/class TestCase\n include FactoryBot::Syntax::Methods/, file) + end + end + + test "creates definition file" do + run_generator + + assert_file app_root("test/factories.rb") do |file| + assert_match definition_file, file + end + end + + test "creates linting test" do + run_generator + + assert_file app_root("test/factory_bots/factories_test.rb") do |file| + assert_match factories_test, file + end + end + + test "creates dev prime task" do + run_generator + + assert_file app_root("lib/tasks/dev.rake") do |file| + assert_match dev_rake, file + end + end + + private + + def prepare_destination + mkdir "test" + touch "Gemfile" + end + + def restore_destination + remove_dir_if_exists "test" + remove_dir_if_exists "spec" + remove_file_if_exists "Gemfile" + remove_dir_if_exists "lib/tasks" + end + + def test_helper + <<-RUBY + ENV["RAILS_ENV"] ||= "test" + require_relative "../config/environment" + require "rails/test_help" + + module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... + end + end + RUBY + end + + def factories_test + <<~RUBY + require "test_helper" + + class FactoryBotsTest < ActiveSupport::TestCase + class FactoryLintingTest < FactoryBotsTest + test "linting of factories" do + FactoryBot.lint traits: true + end + end + end + RUBY + end + + def definition_file + <<~RUBY + FactoryBot.define do + end + RUBY + end + + def dev_rake + <<~RUBY + if Rails.env.development? || Rails.env.test? + require "factory_bot" + + namespace :dev do + desc "Sample data for local development environment" + task prime: "db:setup" do + include FactoryBot::Syntax::Methods + + # create(:user, email: "user@example.com", password: "password") + end + end + end + RUBY + end + end + + class FactoryBotGenerator::RSpecTest < Rails::Generators::TestCase + include Suspenders::TestHelpers + + tests Suspenders::Generators::FactoryBotGenerator + destination Rails.root + setup :prepare_destination + teardown :restore_destination + + test "includes syntax methods" do + touch("spec/rails_helper.rb") + + run_generator + + assert_file app_root("spec/support/factory_bot.rb") do |file| + assert_match factory_bot_config, file + end + assert_file app_root("spec/rails_helper.rb") do |file| + assert_match(/Dir\[Rails\.root\.join\("spec\/support\/\*\*\/\*\.rb"\)\]\.sort\.each { \|file\| require file }/, file) + end + end + + test "creates definition file" do + run_generator + + assert_file app_root("spec/factories.rb") do |file| + assert_match definition_file, file + end + end + + private + + def prepare_destination + mkdir "spec" + touch "Gemfile" + end + + def restore_destination + remove_dir_if_exists "test" + remove_dir_if_exists "spec" + remove_file_if_exists "Gemfile" + remove_dir_if_exists "lib/tasks" + end + + def factory_bot_config + <<~RUBY + FactoryBot.use_parent_strategy = true + + RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods + end + RUBY + end + + def definition_file + <<~RUBY + FactoryBot.define do + end + RUBY + end + end + end +end