diff --git a/NEWS.md b/NEWS.md index 1a31f1e23..31563045d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,6 +14,7 @@ Unreleased * Introduce `suspenders:tasks` generator * Introduce `suspenders:db:migrate` task * Introduce `suspenders:email` generator +* Introduce `suspenders:testing` generator 20230113.0 (January, 13, 2023) diff --git a/README.md b/README.md index 0f1a43568..adf6305e9 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,20 @@ Configures `default_url_options` in `test` and `development`. bin/rails g suspenders:email ``` +### Testing + +Set up the project for an in-depth test-driven development workflow. + +Installs and configures [rspec-rails][], +[action_dispatch-testing-integration-capybara][], [shoulda-matchers][], +[webdrivers][] and [webmock][]. + +[rspec-rails]: https://github.com/rspec/rspec-rails +[action_dispatch-testing-integration-capybara]: https://github.com/thoughtbot/action_dispatch-testing-integration-capybara +[shoulda-matchers]: https://github.com/thoughtbot/shoulda-matchers +[webdrivers]: https://github.com/titusfortner/webdrivers +[webmock]: https://github.com/bblimke/webmock + ## Contributing See the [CONTRIBUTING] document. diff --git a/lib/generators/suspenders/testing_generator.rb b/lib/generators/suspenders/testing_generator.rb new file mode 100644 index 000000000..73d7e909b --- /dev/null +++ b/lib/generators/suspenders/testing_generator.rb @@ -0,0 +1,84 @@ +module Suspenders + module Generators + class TestingGenerator < Rails::Generators::Base + source_root File.expand_path("../../templates/testing", __FILE__) + desc "Set up the project for an in-depth test-driven development workflow." + + def add_gems + gem_group :development, :test do + gem "rspec-rails", "~> 6.1.0" + end + + gem_group :test do + gem "action_dispatch-testing-integration-capybara", + github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.0", + require: "action_dispatch/testing/integration/capybara/rspec" + gem "shoulda-matchers", "~> 6.0" + gem "webdrivers" + gem "webmock" + end + + Bundler.with_unbundled_env { run "bundle install" } + end + + def run_rspec_installation_script + rails_command "generate rspec:install" + end + + def modify_rails_helper + insert_into_file "spec/rails_helper.rb", + "\s\sconfig.infer_base_class_for_anonymous_controllers = false\n", + after: "RSpec.configure do |config|\n" + end + + def modify_spec_helper + persistence_file_path = "\s\sconfig.example_status_persistence_file_path = \"tmp/rspec_examples.txt\"\n" + order = "\s\sconfig.order = :random\n\n" + webmock_config = <<~RUBY + + WebMock.disable_net_connect!( + allow_localhost: true, + allow: [ + /(chromedriver|storage).googleapis.com/, + "googlechromelabs.github.io", + ] + ) + RUBY + + insert_into_file "spec/spec_helper.rb", + persistence_file_path + order, + after: "RSpec.configure do |config|\n" + + insert_into_file "spec/spec_helper.rb", "require \"webmock/rspec\"\n\n", before: "RSpec.configure do |config|" + insert_into_file "spec/spec_helper.rb", webmock_config + end + + def create_system_spec_dir + empty_directory "spec/system" + create_file "spec/system/.gitkeep" + end + + def configure_chromedriver + copy_file "chromedriver.rb", "spec/support/chromedriver.rb" + end + + def configure_i18n_helper + copy_file "i18n.rb", "spec/support/i18n.rb" + end + + def configure_shoulda_matchers + copy_file "shoulda_matchers.rb", "spec/support/shoulda_matchers.rb" + end + + def configure_action_mailer_helpers + # https://guides.rubyonrails.org/testing.html#the-basic-test-case + # + # The ActionMailer::Base.deliveries array is only reset automatically in + # ActionMailer::TestCase and ActionDispatch::IntegrationTest tests. If + # you want to have a clean slate outside these test cases, you can reset + # it manually with: ActionMailer::Base.deliveries.clear + copy_file "action_mailer.rb", "spec/support/action_mailer.rb" + end + end + end +end diff --git a/lib/generators/templates/testing/action_mailer.rb b/lib/generators/templates/testing/action_mailer.rb new file mode 100644 index 000000000..b9563a3bc --- /dev/null +++ b/lib/generators/templates/testing/action_mailer.rb @@ -0,0 +1,5 @@ +RSpec.configure do |config| + config.before(:each) do + ActionMailer::Base.deliveries.clear + end +end diff --git a/lib/generators/templates/testing/chromedriver.rb b/lib/generators/templates/testing/chromedriver.rb new file mode 100644 index 000000000..4091d1531 --- /dev/null +++ b/lib/generators/templates/testing/chromedriver.rb @@ -0,0 +1,27 @@ +require "selenium/webdriver" + +Capybara.register_driver :chrome do |app| + Capybara::Selenium::Driver.new(app, browser: :chrome) +end + +Capybara.register_driver :headless_chrome do |app| + options = ::Selenium::WebDriver::Chrome::Options.new + options.headless! + options.add_argument "--window-size=1680,1050" + + Capybara::Selenium::Driver.new app, + browser: :chrome, + options: options +end + +Capybara.javascript_driver = :headless_chrome + +RSpec.configure do |config| + config.before(:each, type: :system) do + driven_by :rack_test + end + + config.before(:each, type: :system, js: true) do + driven_by Capybara.javascript_driver + end +end diff --git a/lib/generators/templates/testing/i18n.rb b/lib/generators/templates/testing/i18n.rb new file mode 100644 index 000000000..0c61ce662 --- /dev/null +++ b/lib/generators/templates/testing/i18n.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include ActionView::Helpers::TranslationHelper +end diff --git a/lib/generators/templates/testing/shoulda_matchers.rb b/lib/generators/templates/testing/shoulda_matchers.rb new file mode 100644 index 000000000..7d045f359 --- /dev/null +++ b/lib/generators/templates/testing/shoulda_matchers.rb @@ -0,0 +1,6 @@ +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end diff --git a/test/generators/suspenders/testing_generator_test.rb b/test/generators/suspenders/testing_generator_test.rb new file mode 100644 index 000000000..ff901cd7e --- /dev/null +++ b/test/generators/suspenders/testing_generator_test.rb @@ -0,0 +1,246 @@ +require "test_helper" +require "generators/suspenders/testing_generator" + +module Suspenders + module Generators + class TestingGeneratorTest < Rails::Generators::TestCase + include Suspenders::TestHelpers + + tests Suspenders::Generators::TestingGenerator + destination Rails.root + setup :prepare_destination + teardown :restore_destination + + test "adds gems to Gemfile" do + expected = <<~RUBY + group :development, :test do + gem "rspec-rails", "~> 6.1.0" + end + + group :test do + gem "action_dispatch-testing-integration-capybara", github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.0", require: "action_dispatch/testing/integration/capybara/rspec" + gem "shoulda-matchers", "~> 6.0" + gem "webdrivers" + gem "webmock" + end + RUBY + + run_generator + + assert_file app_root("Gemfile") do |file| + assert_match(expected, file) + end + end + + test "installs gems with Bundler" do + output = run_generator + + assert_match(/bundle install/, output) + end + + test "runs RSpec installation script" do + output = run_generator + + assert_match(/generate rspec:install/, output) + end + + test "configures rails_helper" do + touch "spec/rails_helper.rb", content: rails_helper + + run_generator + + assert_file "spec/rails_helper.rb" do |file| + assert_match(/RSpec\.configure do \|config\|\s{3}config\.infer_base_class_for_anonymous_controllers\s*=\s*false/m, + file) + end + end + + test "configures spec_helper" do + touch "spec/spec_helper.rb", content: spec_helper + expected = <<~RUBY + require "webmock/rspec" + + RSpec.configure do |config| + config.example_status_persistence_file_path = "tmp/rspec_examples.txt" + config.order = :random + + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + config.shared_context_metadata_behavior = :apply_to_host_groups + end + + WebMock.disable_net_connect!( + allow_localhost: true, + allow: [ + /(chromedriver|storage).googleapis.com/, + "googlechromelabs.github.io", + ] + ) + RUBY + + run_generator + + assert_file app_root("spec/spec_helper.rb") do |file| + assert_equal expected, file + end + end + + test "configures Chromedriver" do + expected = <<~RUBY + require "selenium/webdriver" + + Capybara.register_driver :chrome do |app| + Capybara::Selenium::Driver.new(app, browser: :chrome) + end + + Capybara.register_driver :headless_chrome do |app| + options = ::Selenium::WebDriver::Chrome::Options.new + options.headless! + options.add_argument "--window-size=1680,1050" + + Capybara::Selenium::Driver.new app, + browser: :chrome, + options: options + end + + Capybara.javascript_driver = :headless_chrome + + RSpec.configure do |config| + config.before(:each, type: :system) do + driven_by :rack_test + end + + config.before(:each, type: :system, js: true) do + driven_by Capybara.javascript_driver + end + end + RUBY + + run_generator + + assert_file app_root("spec/support/chromedriver.rb") do |file| + assert_equal expected, file + end + end + + test "creates system spec directory" do + run_generator + + assert_file app_root("spec/system/.gitkeep") + end + + test "configures Should Matchers" do + expected = <<~RUBY + Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end + end + RUBY + + run_generator + + assert_file app_root("spec/support/shoulda_matchers.rb") do |file| + assert_equal expected, file + end + end + + test "configures i18n" do + expected = <<~RUBY + RSpec.configure do |config| + config.include ActionView::Helpers::TranslationHelper + end + RUBY + + run_generator + + assert_file app_root("spec/support/i18n.rb") do |file| + assert_equal expected, file + end + end + + test "configures Action Mailer" do + expected = <<~RUBY + RSpec.configure do |config| + config.before(:each) do + ActionMailer::Base.deliveries.clear + end + end + RUBY + + run_generator + + assert_file app_root("spec/support/action_mailer.rb") do |file| + assert_equal expected, file + end + end + + test "has custom description" do + assert_no_match(/Description/, generator_class.desc) + end + + private + + def prepare_destination + touch "Gemfile" + mkdir "spec" + touch "spec/rails_helper.rb" + touch "spec/spec_helper.rb" + end + + def restore_destination + remove_file_if_exists "Gemfile" + remove_dir_if_exists "spec" + end + + def rails_helper + # Generated from rails g rspec:install + # Comments removed + <<~RUBY + require 'spec_helper' + ENV['RAILS_ENV'] ||= 'test' + require_relative '../config/environment' + + abort("The Rails environment is running in production mode!") if Rails.env.production? + require 'rspec/rails' + + begin + ActiveRecord::Migration.maintain_test_schema! + rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip + end + RSpec.configure do |config| + config.fixture_path = "#{::Rails.root}/spec/fixtures" + config.use_transactional_fixtures = true + config.infer_spec_type_from_file_location! + config.filter_rails_from_backtrace! + end + Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file } + RUBY + end + + def spec_helper + # Generated from rails g rspec:install + # Comments removed + <<~RUBY + RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + config.shared_context_metadata_behavior = :apply_to_host_groups + end + RUBY + end + end + end +end