diff --git a/NEWS.md b/NEWS.md index 3db5e4859..ceac6d1a0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,6 +4,7 @@ Unreleased * Introduce `suspenders:accessibility` generator * Introduce `Suspenders::Generators::APIAppUnsupported` module and concern * Introduce `suspenders:inline_svg` generator +* Introduce `suspenders:factories` generator 20230113.0 (January, 13, 2023) diff --git a/README.md b/README.md index 58e7136fb..134520935 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,27 @@ Installs [capybara_accessibility_audit] and [capybara_accessible_selectors] [capybara_accessibility_audit]: https://github.com/thoughtbot/capybara_accessibility_audit [capybara_accessible_selectors]: https://github.com/citizensadvice/capybara_accessible_selectors +### Factories + +Build test data with clarity and ease. + +This uses [FactoryBot] to help you define dummy and test data for your +test suite. The `create`, `build`, and `build_stubbed` class methods are +directly available to all tests. + +We recommend putting FactoryBot definitions in one `spec/factories.rb` +(or `test/factories`) file, at least until it grows unwieldy. This helps reduce +confusion around circular dependencies and makes it easy to jump between +definitions. + +Supports the [default test suite] and [RSpec]. + +`bin/rails g suspenders:factories` + + [Factory Bot]: https://github.com/thoughtbot/factory_bot_rails + [default test suite]: https://guides.rubyonrails.org/testing.html + [RSpec]: https://rspec.info + ### Inline SVG Render SVG images inline using the [inline_svg] gem, as a potential performance diff --git a/lib/generators/suspenders/factories_generator.rb b/lib/generators/suspenders/factories_generator.rb new file mode 100644 index 000000000..f471fb0fb --- /dev/null +++ b/lib/generators/suspenders/factories_generator.rb @@ -0,0 +1,66 @@ +module Suspenders + module Generators + class FactoriesGenerator < Rails::Generators::Base + include Suspenders::Generators::Helpers + + source_root File.expand_path("../../templates/factories", __FILE__) + desc <<~TEXT + Build test data with clarity and ease. + + This uses FactoryBot to help you define dummy and test data for your test + suite. The `create`, `build`, and `build_stubbed` class methods are directly + available to all tests. + + We recommend putting FactoryBot definitions in one `spec/factories.rb` (or + `test/factories`) file, at least until it grows unwieldy. This helps reduce + confusion around circular dependencies and makes it easy to jump between + definitions. + + Supports the default test suite and RSpec. + TEXT + + 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 + end + end +end diff --git a/lib/generators/templates/factories/factories.rb b/lib/generators/templates/factories/factories.rb new file mode 100644 index 000000000..3bfcbd203 --- /dev/null +++ b/lib/generators/templates/factories/factories.rb @@ -0,0 +1,2 @@ +FactoryBot.define do +end diff --git a/lib/generators/templates/factories/factories_spec.rb b/lib/generators/templates/factories/factories_spec.rb new file mode 100644 index 000000000..91418c607 --- /dev/null +++ b/lib/generators/templates/factories/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/factories/factories_test.rb b/lib/generators/templates/factories/factories_test.rb new file mode 100644 index 000000000..23b78663d --- /dev/null +++ b/lib/generators/templates/factories/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/factories/factory_bot_rspec.rb b/lib/generators/templates/factories/factory_bot_rspec.rb new file mode 100644 index 000000000..4943f172e --- /dev/null +++ b/lib/generators/templates/factories/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/lib/suspenders/generators.rb b/lib/suspenders/generators.rb index 892433679..cbefec2bf 100644 --- a/lib/suspenders/generators.rb +++ b/lib/suspenders/generators.rb @@ -2,6 +2,24 @@ module Suspenders module Generators + module Helpers + def default_test_suite? + File.exist? Rails.root.join("test") + end + + def rspec_test_suite? + File.exist? Rails.root.join("spec/spec_helper.rb") + 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 + module APIAppUnsupported class Error < StandardError def message diff --git a/test/generators/suspenders/factories_generator_test.rb b/test/generators/suspenders/factories_generator_test.rb new file mode 100644 index 000000000..cad2fb70c --- /dev/null +++ b/test/generators/suspenders/factories_generator_test.rb @@ -0,0 +1,225 @@ +require "test_helper" +require "generators/suspenders/factories_generator" + +module Suspenders + module Generators + class FactoriesGenerator::DefaultTest < Rails::Generators::TestCase + include Suspenders::TestHelpers + + tests Suspenders::Generators::FactoriesGenerator + destination Rails.root + setup :prepare_destination + teardown :restore_destination + + test "generator has a description" do + description = <<~TEXT + Build test data with clarity and ease. + + This uses FactoryBot to help you define dummy and test data for your test + suite. The `create`, `build`, and `build_stubbed` class methods are directly + available to all tests. + + We recommend putting FactoryBot definitions in one `spec/factories.rb` (or + `test/factories`) file, at least until it grows unwieldy. This helps reduce + confusion around circular dependencies and makes it easy to jump between + definitions. + + Supports the default test suite and RSpec. + TEXT + + assert_equal description, FactoriesGenerator.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 FactoriesGenerator::RSpecTest < Rails::Generators::TestCase + include Suspenders::TestHelpers + + tests Suspenders::Generators::FactoriesGenerator + 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 "does not modify rails_helper if it's configured to include support files" do + touch("spec/rails_helper.rb") + rails_helper = <<~RUBY + Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file } + RUBY + File.open(app_root("spec/rails_helper.rb"), "w") { _1.write rails_helper } + + run_generator + + assert_file app_root("spec/rails_helper.rb") do |file| + assert_equal rails_helper, 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 "spec/spec_helper.rb" + 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