diff --git a/NEWS.md b/NEWS.md index 2dfddb1a0..88cb07a4a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ Unreleased * Remove `suspenders` system executable +* Introduce `suspenders:factory_bot` generator 20230113.0 (January, 13, 2023) diff --git a/README.md b/README.md index 5a9e7f9f9..0c49b7a17 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,15 @@ end ## Generators -TODO +### Factory Bot + +Installs and configures [factory_bot_rails]. Supports the [default test suite] and [RSpec]. + +`./bin/rails g suspenders:factory_bot` + + [factory_bot_rails]: https://github.com/thoughtbot/factory_bot_rails + [default test suite]: https://guides.rubyonrails.org/testing.html + [RSpec]: https://rspec.info ## Contributing diff --git a/lib/generators/suspenders/factory_bot_generator.rb b/lib/generators/suspenders/factory_bot_generator.rb new file mode 100644 index 000000000..2cea1b771 --- /dev/null +++ b/lib/generators/suspenders/factory_bot_generator.rb @@ -0,0 +1,69 @@ +module Suspenders + module Generators + class FactoryBotGenerator < Rails::Generators::Base + source_root File.expand_path("../../templates/factory_bot", __FILE__) + desc "Installs and configures factory_bot_rails. Supports the default test suite and RSpec." + + 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 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" + elsif rspec_test_suite? + copy_file "factories_spec.rb", "spec/factory_bots/factories_spec.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/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_spec.rb b/lib/generators/templates/factory_bot/factories_spec.rb new file mode 100644 index 000000000..91418c607 --- /dev/null +++ b/lib/generators/templates/factory_bot/factories_spec.rb @@ -0,0 +1,7 @@ +require "rails_helper" + +RSpec.describe "Factories" do + it "has valid factoties" do + FactoryBot.lint traits: true + end +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..318d12424 --- /dev/null +++ b/test/generators/suspenders/factory_bot_generator_test.rb @@ -0,0 +1,203 @@ +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 "generator has a description" do + description = "Installs and configures factory_bot_rails. Supports the default test suite and RSpec." + + assert_equal description, FactoryBotGenerator.desc + 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 + definition_file = <<~RUBY + FactoryBot.define do + end + RUBY + + run_generator + + assert_file app_root("test/factories.rb") do |file| + assert_match definition_file, file + end + end + + test "creates linting test" do + 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 + + run_generator + + assert_file app_root("test/factory_bots/factories_test.rb") do |file| + assert_match factories_test, file + end + end + + private + + def prepare_destination + mkdir "test" + touch "Gemfile" + end + + def restore_destination + remove_dir_if_exists "test" + 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 + 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") + factory_bot_config = <<~RUBY + FactoryBot.use_parent_strategy = true + + RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods + end + RUBY + + 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 + definition_file = <<~RUBY + FactoryBot.define do + end + RUBY + + run_generator + + assert_file app_root("spec/factories.rb") do |file| + assert_match definition_file, file + end + end + + test "creates linting test" do + factories_spec = <<~RUBY + require "rails_helper" + + RSpec.describe "Factories" do + it "has valid factoties" do + FactoryBot.lint traits: true + end + end + RUBY + + run_generator + + assert_file app_root("spec/factory_bots/factories_spec.rb") do |file| + assert_match factories_spec, file + end + end + + private + + def prepare_destination + mkdir "spec" + touch "Gemfile" + end + + def restore_destination + remove_dir_if_exists "spec" + remove_file_if_exists "Gemfile" + remove_dir_if_exists "lib/tasks" + end + end + end +end