Skip to content

Commit 314dd72

Browse files
Introduce suspenders:factories 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 uses RSpec and 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
1 parent 04ea752 commit 314dd72

File tree

9 files changed

+354
-0
lines changed

9 files changed

+354
-0
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Unreleased
44
* Introduce `suspenders:accessibility` generator
55
* Introduce `Suspenders::Generators::APIAppUnsupported` module and concern
66
* Introduce `suspenders:inline_svg` generator
7+
* Introduce `suspenders:factories` generator
78

89
20230113.0 (January, 13, 2023)
910

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ Installs [capybara_accessibility_audit] and [capybara_accessible_selectors]
3030
[capybara_accessibility_audit]: https://github.com/thoughtbot/capybara_accessibility_audit
3131
[capybara_accessible_selectors]: https://github.com/citizensadvice/capybara_accessible_selectors
3232

33+
### Factories
34+
35+
Build test data with clarity and ease.
36+
37+
This uses [FactoryBot] to help you define dummy and test data for your
38+
test suite. The `create`, `build`, and `build_stubbed` class methods are
39+
directly available to all tests.
40+
41+
We recommend putting FactoryBot definitions in one `spec/factories.rb`
42+
(or `test/factories`) file, at least until it grows unwieldy. This helps reduce
43+
confusion around circular dependencies and makes it easy to jump between
44+
definitions.
45+
46+
Supports the [default test suite] and [RSpec].
47+
48+
`bin/rails g suspenders:factories`
49+
50+
[Factory Bot]: https://github.com/thoughtbot/factory_bot_rails
51+
[default test suite]: https://guides.rubyonrails.org/testing.html
52+
[RSpec]: https://rspec.info
53+
3354
### Inline SVG
3455

3556
Render SVG images inline using the [inline_svg] gem, as a potential performance
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
module Suspenders
2+
module Generators
3+
class FactoriesGenerator < Rails::Generators::Base
4+
include Suspenders::Generators::Helpers
5+
6+
source_root File.expand_path("../../templates/factories", __FILE__)
7+
desc <<~TEXT
8+
Build test data with clarity and ease.
9+
10+
This uses FactoryBot to help you define dummy and test data for your test
11+
suite. The `create`, `build`, and `build_stubbed` class methods are directly
12+
available to all tests.
13+
14+
We recommend putting FactoryBot definitions in one `spec/factories.rb` (or
15+
`test/factories`) file, at least until it grows unwieldy. This helps reduce
16+
confusion around circular dependencies and makes it easy to jump between
17+
definitions.
18+
19+
Supports the default test suite and RSpec.
20+
TEXT
21+
22+
def add_factory_bot
23+
gem_group :development, :test do
24+
gem "factory_bot_rails"
25+
end
26+
27+
Bundler.with_unbundled_env { run "bundle install" }
28+
end
29+
30+
def set_up_factory_bot
31+
if default_test_helper_present?
32+
insert_into_file Rails.root.join("test/test_helper.rb"), after: "class TestCase" do
33+
"\n include FactoryBot::Syntax::Methods"
34+
end
35+
elsif rspec_test_helper_present?
36+
copy_file "factory_bot_rspec.rb", "spec/support/factory_bot.rb"
37+
insert_into_file Rails.root.join("spec/rails_helper.rb") do
38+
%(Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file })
39+
end
40+
end
41+
end
42+
43+
def generate_empty_factories_file
44+
if default_test_suite?
45+
copy_file "factories.rb", "test/factories.rb"
46+
elsif rspec_test_suite?
47+
copy_file "factories.rb", "spec/factories.rb"
48+
end
49+
end
50+
51+
def remove_fixture_definitions
52+
if default_test_helper_present?
53+
comment_lines "test/test_helper.rb", /fixtures :all/
54+
end
55+
end
56+
57+
def create_linting_test
58+
if default_test_suite?
59+
copy_file "factories_test.rb", "test/factory_bots/factories_test.rb"
60+
elsif rspec_test_suite?
61+
copy_file "factories_spec.rb", "spec/factory_bots/factories_spec.rb"
62+
end
63+
end
64+
end
65+
end
66+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FactoryBot.define do
2+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require "rails_helper"
2+
3+
RSpec.describe "Factories" do
4+
it "has valid factoties" do
5+
FactoryBot.lint traits: true
6+
end
7+
end
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
require "test_helper"
2+
3+
class FactoryBotsTest < ActiveSupport::TestCase
4+
class FactoryLintingTest < FactoryBotsTest
5+
test "linting of factories" do
6+
FactoryBot.lint traits: true
7+
end
8+
end
9+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FactoryBot.use_parent_strategy = true
2+
3+
RSpec.configure do |config|
4+
config.include FactoryBot::Syntax::Methods
5+
end

lib/suspenders/generators.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
module Suspenders
44
module Generators
5+
module Helpers
6+
def default_test_suite?
7+
File.exist? Rails.root.join("test")
8+
end
9+
10+
def rspec_test_suite?
11+
File.exist? Rails.root.join("spec/spec_helper.rb")
12+
end
13+
14+
def default_test_helper_present?
15+
File.exist? Rails.root.join("test/test_helper.rb")
16+
end
17+
18+
def rspec_test_helper_present?
19+
File.exist? Rails.root.join("spec/rails_helper.rb")
20+
end
21+
end
22+
523
module APIAppUnsupported
624
class Error < StandardError
725
def message
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
require "test_helper"
2+
require "generators/suspenders/factories_generator"
3+
4+
module Suspenders
5+
module Generators
6+
class FactoriesGenerator::DefaultTest < Rails::Generators::TestCase
7+
include Suspenders::TestHelpers
8+
9+
tests Suspenders::Generators::FactoriesGenerator
10+
destination Rails.root
11+
setup :prepare_destination
12+
teardown :restore_destination
13+
14+
test "generator has a description" do
15+
description = <<~TEXT
16+
Build test data with clarity and ease.
17+
18+
This uses FactoryBot to help you define dummy and test data for your test
19+
suite. The `create`, `build`, and `build_stubbed` class methods are directly
20+
available to all tests.
21+
22+
We recommend putting FactoryBot definitions in one `spec/factories.rb` (or
23+
`test/factories`) file, at least until it grows unwieldy. This helps reduce
24+
confusion around circular dependencies and makes it easy to jump between
25+
definitions.
26+
27+
Supports the default test suite and RSpec.
28+
TEXT
29+
30+
assert_equal description, FactoriesGenerator.desc
31+
end
32+
33+
test "installs gem with Bundler" do
34+
Bundler.stubs(:with_unbundled_env).yields
35+
generator.expects(:run).with("bundle install").once
36+
37+
capture(:stdout) do
38+
generator.add_factory_bot
39+
end
40+
end
41+
42+
test "removes fixture definitions" do
43+
File.open(app_root("test/test_helper.rb"), "w") { _1.write test_helper }
44+
45+
run_generator
46+
47+
assert_file app_root("test/test_helper.rb") do |file|
48+
assert_match(/# fixtures :all/, file)
49+
end
50+
end
51+
52+
test "adds gem to Gemfile" do
53+
run_generator
54+
55+
assert_file app_root("Gemfile") do |file|
56+
assert_match(/group :development, :test do\n gem "factory_bot_rails"\nend/, file)
57+
end
58+
end
59+
60+
test "includes syntax methods" do
61+
File.open(app_root("test/test_helper.rb"), "w") { _1.write test_helper }
62+
63+
run_generator
64+
65+
assert_file app_root("test/test_helper.rb") do |file|
66+
assert_match(/class TestCase\n include FactoryBot::Syntax::Methods/, file)
67+
end
68+
end
69+
70+
test "creates definition file" do
71+
definition_file = <<~RUBY
72+
FactoryBot.define do
73+
end
74+
RUBY
75+
76+
run_generator
77+
78+
assert_file app_root("test/factories.rb") do |file|
79+
assert_match definition_file, file
80+
end
81+
end
82+
83+
test "creates linting test" do
84+
factories_test = <<~RUBY
85+
require "test_helper"
86+
87+
class FactoryBotsTest < ActiveSupport::TestCase
88+
class FactoryLintingTest < FactoryBotsTest
89+
test "linting of factories" do
90+
FactoryBot.lint traits: true
91+
end
92+
end
93+
end
94+
RUBY
95+
96+
run_generator
97+
98+
assert_file app_root("test/factory_bots/factories_test.rb") do |file|
99+
assert_match factories_test, file
100+
end
101+
end
102+
103+
private
104+
105+
def prepare_destination
106+
mkdir "test"
107+
touch "Gemfile"
108+
end
109+
110+
def restore_destination
111+
remove_dir_if_exists "test"
112+
remove_file_if_exists "Gemfile"
113+
remove_dir_if_exists "lib/tasks"
114+
end
115+
116+
def test_helper
117+
<<~RUBY
118+
ENV["RAILS_ENV"] ||= "test"
119+
require_relative "../config/environment"
120+
require "rails/test_help"
121+
122+
module ActiveSupport
123+
class TestCase
124+
# Run tests in parallel with specified workers
125+
parallelize(workers: :number_of_processors)
126+
127+
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
128+
fixtures :all
129+
130+
# Add more helper methods to be used by all tests here...
131+
end
132+
end
133+
RUBY
134+
end
135+
end
136+
137+
class FactoriesGenerator::RSpecTest < Rails::Generators::TestCase
138+
include Suspenders::TestHelpers
139+
140+
tests Suspenders::Generators::FactoriesGenerator
141+
destination Rails.root
142+
setup :prepare_destination
143+
teardown :restore_destination
144+
145+
test "includes syntax methods" do
146+
touch("spec/rails_helper.rb")
147+
factory_bot_config = <<~RUBY
148+
FactoryBot.use_parent_strategy = true
149+
150+
RSpec.configure do |config|
151+
config.include FactoryBot::Syntax::Methods
152+
end
153+
RUBY
154+
155+
run_generator
156+
157+
assert_file app_root("spec/support/factory_bot.rb") do |file|
158+
assert_match factory_bot_config, file
159+
end
160+
assert_file app_root("spec/rails_helper.rb") do |file|
161+
assert_match(/Dir\[Rails\.root\.join\("spec\/support\/\*\*\/\*\.rb"\)\]\.sort\.each { \|file\| require file }/, file)
162+
end
163+
end
164+
165+
test "creates definition file" do
166+
definition_file = <<~RUBY
167+
FactoryBot.define do
168+
end
169+
RUBY
170+
171+
run_generator
172+
173+
assert_file app_root("spec/factories.rb") do |file|
174+
assert_match definition_file, file
175+
end
176+
end
177+
178+
test "does not modify rails_helper if it's configured to include support files" do
179+
touch("spec/rails_helper.rb")
180+
rails_helper = <<~RUBY
181+
Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file }
182+
RUBY
183+
File.open(app_root("spec/rails_helper.rb"), "w") { _1.write rails_helper }
184+
185+
run_generator
186+
187+
assert_file app_root("spec/rails_helper.rb") do |file|
188+
assert_equal rails_helper, file
189+
end
190+
end
191+
192+
test "creates linting test" do
193+
factories_spec = <<~RUBY
194+
require "rails_helper"
195+
196+
RSpec.describe "Factories" do
197+
it "has valid factoties" do
198+
FactoryBot.lint traits: true
199+
end
200+
end
201+
RUBY
202+
203+
run_generator
204+
205+
assert_file app_root("spec/factory_bots/factories_spec.rb") do |file|
206+
assert_match factories_spec, file
207+
end
208+
end
209+
210+
private
211+
212+
def prepare_destination
213+
mkdir "spec"
214+
touch "spec/spec_helper.rb"
215+
touch "Gemfile"
216+
end
217+
218+
def restore_destination
219+
remove_dir_if_exists "spec"
220+
remove_file_if_exists "Gemfile"
221+
remove_dir_if_exists "lib/tasks"
222+
end
223+
end
224+
end
225+
end

0 commit comments

Comments
 (0)