Skip to content

Commit

Permalink
Introduce suspenders:factories generator (#1136)
Browse files Browse the repository at this point in the history
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 uses RSpec or the
[default Rails test suite][].

Adds generator which adds a test to lint all Factories in an effort to
improve developer experience.

Additionally, we remove the generation of the `dev:prime` task as we
felt that should be the responsibly of another generator.

[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
  • Loading branch information
stevepolitodesign authored Nov 17, 2023
1 parent 04ea752 commit 4f20aa8
Show file tree
Hide file tree
Showing 9 changed files with 354 additions and 0 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions lib/generators/suspenders/factories_generator.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions lib/generators/templates/factories/factories.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FactoryBot.define do
end
7 changes: 7 additions & 0 deletions lib/generators/templates/factories/factories_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require "rails_helper"

RSpec.describe "Factories" do
it "has valid factoties" do
FactoryBot.lint traits: true
end
end
9 changes: 9 additions & 0 deletions lib/generators/templates/factories/factories_test.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions lib/generators/templates/factories/factory_bot_rspec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FactoryBot.use_parent_strategy = true

RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
18 changes: 18 additions & 0 deletions lib/suspenders/generators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
225 changes: 225 additions & 0 deletions test/generators/suspenders/factories_generator_test.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 4f20aa8

Please sign in to comment.