diff --git a/config/default.yml b/config/default.yml index 55a0736..702b6e3 100644 --- a/config/default.yml +++ b/config/default.yml @@ -192,6 +192,12 @@ Minitest/NoTestCases: Enabled: false VersionAdded: '0.30' +Minitest/NonExecutableTestMethod: + Description: 'Checks uses of test methods outside test class.' + Enabled: pending + Severity: warning + VersionAdded: '<>' + Minitest/NonPublicTestMethod: Description: 'Detects non `public` (marked as `private` or `protected`) test methods.' Enabled: pending diff --git a/lib/rubocop/cop/minitest/non_executable_test_method.rb b/lib/rubocop/cop/minitest/non_executable_test_method.rb new file mode 100644 index 0000000..316acb0 --- /dev/null +++ b/lib/rubocop/cop/minitest/non_executable_test_method.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Minitest + # Checks for the use of test methods outside of a test class. + # + # Test methods should be defined within a test class to ensure their execution. + # + # NOTE: This cop assumes that classes whose superclass name includes the word + # "`Test`" are test classes, in order to prevent false positives. + # + # @example + # + # # bad + # class FooTest < Minitest::Test + # end + # def test_method_should_be_inside_test_class + # end + # + # # good + # class FooTest < Minitest::Test + # def test_method_should_be_inside_test_class + # end + # end + # + class NonExecutableTestMethod < Base + include MinitestExplorationHelpers + + MSG = 'Test method should be defined inside a test class to ensure execution.' + + def on_def(node) + return if !test_method?(node) || !use_test_class? + return if node.left_siblings.none? { |sibling| possible_test_class?(sibling) } + + add_offense(node) + end + + def use_test_class? + root_node = processed_source.ast + + root_node.each_descendant(:class).any? { |class_node| test_class?(class_node) } + end + + def possible_test_class?(node) + node.is_a?(AST::ClassNode) && test_class?(node) && node.parent_class.source.include?('Test') + end + end + end + end +end diff --git a/lib/rubocop/cop/minitest_cops.rb b/lib/rubocop/cop/minitest_cops.rb index 2a5ea13..014a861 100644 --- a/lib/rubocop/cop/minitest_cops.rb +++ b/lib/rubocop/cop/minitest_cops.rb @@ -30,6 +30,7 @@ require_relative 'minitest/assert_truthy' require_relative 'minitest/duplicate_test_run' require_relative 'minitest/empty_line_before_assertion_methods' +require_relative 'minitest/non_executable_test_method' require_relative 'minitest/redundant_message_argument' require_relative 'minitest/return_in_test_method' require_relative 'minitest/test_file_name' diff --git a/test/rubocop/cop/minitest/non_executable_test_method_test.rb b/test/rubocop/cop/minitest/non_executable_test_method_test.rb new file mode 100644 index 0000000..e146bff --- /dev/null +++ b/test/rubocop/cop/minitest/non_executable_test_method_test.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +require_relative '../../../test_helper' + +class NonExecutableTestMethodTest < Minitest::Test + def test_registers_offense_when_test_method_is_defined_outside_minitest_test_class + assert_offense(<<~RUBY) + class FooTest < Minitest::Test + end + def test_foo + ^^^^^^^^^^^^ Test method should be defined inside a test class to ensure execution. + end + RUBY + end + + def test_registers_offense_when_test_method_is_defined_outside_active_support_test_case_class + assert_offense(<<~RUBY) + class FooTest < ActiveSupport::TestCase + end + + def test_foo + ^^^^^^^^^^^^ Test method should be defined inside a test class to ensure execution. + end + RUBY + end + + def test_does_not_register_offense_when_test_method_is_defined_outside_namespaced_test_class + assert_offense(<<~RUBY) + module M + class FooTest < Minitest::Test + def test_foo + end + end + + def test_bar + ^^^^^^^^^^^^ Test method should be defined inside a test class to ensure execution. + end + end + RUBY + end + + def test_does_not_register_offense_when_non_test_method_is_defined_outside_test_class + assert_no_offenses(<<~RUBY) + class FooTest < Minitest::Test + end + def do_something + end + RUBY + end + + def test_does_not_register_offense_when_non_test_method_is_defined_outside_active_support_test_case_class + assert_no_offenses(<<~RUBY) + class FooTest < ActiveSupport::TestCase + end + def do_something + end + RUBY + end + + def test_does_not_register_offense_when_test_method_is_defined_inside_test_class + assert_no_offenses(<<~RUBY) + class FooTest < Minitest::Test + def test_foo + end + end + RUBY + end + + def test_does_not_register_offense_when_test_method_is_defined_inside_test_helper_class + assert_no_offenses(<<~RUBY) + class FooTest < Minitest::Test + def test_foo + end + end + + class TestHelperMailer < ActionMailer::Base + def test_parameter_args + end + end + RUBY + end + + def test_does_not_register_offense_when_test_method_is_defined_inside_test_helper_module + assert_no_offenses(<<~RUBY) + class FooTest < Minitest::Test + def test_foo + end + end + + module TestHelper + def test_parameter_args + end + end + RUBY + end + + def test_does_not_register_offense_when_test_class_in_which_test_method_is_defined_is_repeated + assert_no_offenses(<<~RUBY) + class FooTest < Minitest::Test + def test_foo + end + end + + class FooTest < Minitest::Test + def test_foo + end + end + RUBY + end + + def test_does_not_register_offense_when_test_method_is_defined_and_test_class_is_not_defined + assert_no_offenses(<<~RUBY) + def test_something + end + RUBY + end + + def test_does_not_register_offense_when_test_method_is_defined_inside_condition + assert_no_offenses(<<~RUBY) + module ActiveRecord + class AdapterTest < ActiveRecord::TestCase + unless current_adapter?(:PostgreSQLAdapter) + def test_update_prepared_statement + end + end + end + end + RUBY + end + + def test_does_not_register_offense_when_nested_test_method_and_two_test_classes_are_defined + assert_no_offenses(<<~RUBY) + class FooTest < ActiveRecord::TestCase + def test_foo + def test_bar + end + end + end + + class BarTest < ActiveRecord::TestCase + end + RUBY + end + + def test_does_not_register_offense_when_non_test_case_class_is_defined_before_test_method + assert_no_offenses(<<~RUBY) + class SetTest < ActiveRecord::AbstractMysqlTestCase + class SetTest < ActiveRecord::Base + end + + def test_should_not_be_unsigned + column = SetTest.columns_hash["set_column"] + assert_not_predicate column, :unsigned? + end + end + RUBY + end +end