diff --git a/changelog/new_duplicated_test_run_cop.md b/changelog/new_duplicated_test_run_cop.md new file mode 100644 index 00000000..caf95415 --- /dev/null +++ b/changelog/new_duplicated_test_run_cop.md @@ -0,0 +1 @@ +* [#164](https://github.com/rubocop/rubocop-minitest/pull/164): Add new `Minitest/DuplicatedTestRun cop. ([@ignacio-chiazzo][]) diff --git a/config/default.yml b/config/default.yml index dd836ca6..427ca6d9 100644 --- a/config/default.yml +++ b/config/default.yml @@ -105,6 +105,11 @@ Minitest/AssertWithExpectedArgument: Safe: false VersionAdded: '0.11' +Minitest/DuplicatedTestRun: + Description: 'This cop detects duplicate test runs caused by one test class inheriting from another.' + Enabled: pending + VersionAdded: '<>' + Minitest/GlobalExpectations: Description: 'This cop checks for deprecated global expectations.' StyleGuide: 'https://minitest.rubystyle.guide#global-expectations' diff --git a/lib/rubocop/cop/minitest/duplicated_test_run.rb b/lib/rubocop/cop/minitest/duplicated_test_run.rb new file mode 100644 index 00000000..f41a9868 --- /dev/null +++ b/lib/rubocop/cop/minitest/duplicated_test_run.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Minitest + # If a Minitest class inherits from another class, + # it will also inherit its methods causing Minitest to run the parent's tests methods twice. + # + # This cop detects when there are two tests classes, one inherits from the other, and both have tests methods. + # This cop will add an offence to the Child class in such a case. + # + # @example + # # bad + # class ParentTest < Minitest::Test + # def test_parent # it will run this test twice. + # end + # end + # + # class ChildTest < ParentTest + # def test_child + # end + # end + # + # + # # good + # class ParentTest < Minitest::Test + # def test_parent + # end + # end + # + # class ChildTest < Minitest::Test + # def test_child + # end + # end + # + # # good + # class ParentTest < Minitest::Test + # end + # + # class ChildTest + # def test_child + # end + # + # def test_parent + # end + # end + # + class DuplicatedTestRun < Base + include MinitestExplorationHelpers + + MSG = "Subclasses with test methods causes the parent' tests to run them twice." + + def on_class(class_node) + return unless test_class?(class_node) + return unless test_methods?(class_node) + return unless parent_class_has_test_methods(class_node) + + message = format(MSG) + add_offense(class_node, message: message) + end + + private + + def parent_class_has_test_methods(class_node) + parent_class = class_node.parent_class + parent_class_node = class_node.parent.each_child_node(:class).detect do |klass| + klass.identifier == parent_class + end + + return false unless parent_class_node + + test_methods?(parent_class_node) + end + + def test_methods?(class_node) + test_cases(class_node).size.positive? + end + end + end + end +end diff --git a/lib/rubocop/cop/minitest_cops.rb b/lib/rubocop/cop/minitest_cops.rb index f2a8516f..ab7cfcf8 100644 --- a/lib/rubocop/cop/minitest_cops.rb +++ b/lib/rubocop/cop/minitest_cops.rb @@ -23,6 +23,7 @@ require_relative 'minitest/assert_respond_to' require_relative 'minitest/assert_silent' require_relative 'minitest/assert_truthy' +require_relative 'minitest/duplicated_test_run' require_relative 'minitest/global_expectations' require_relative 'minitest/literal_as_actual_argument' require_relative 'minitest/multiple_assertions' diff --git a/test/rubocop/cop/minitest/duplicated_test_run_test.rb b/test/rubocop/cop/minitest/duplicated_test_run_test.rb new file mode 100644 index 00000000..00fb9803 --- /dev/null +++ b/test/rubocop/cop/minitest/duplicated_test_run_test.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'test_helper' + +class DuplicatedTestRunTest < Minitest::Test + def test_registers_offense_when_parent_and_child_have_tests_methods + assert_offense(<<~RUBY) + class ParentTest < Minitest::Test + def test_parent + end + end + + class ChildTest < ParentTest + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Subclasses with test methods causes the parent' tests to run them twice. + def test_child_asserts_twice + assert_equal(1, 1) + end + end + RUBY + end + + def test_registers_offense_when_parent_and_children_have_tests_methods + assert_offense(<<~RUBY) + class ParentTest < Minitest::Test + def test_parent + end + end + + class Child1Test < ParentTest + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Subclasses with test methods causes the parent' tests to run them twice. + def test_parent + end + end + + class Child2Test < ParentTest + end + + class Child3Test < ParentTest + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Subclasses with test methods causes the parent' tests to run them twice. + def test_1_child_2_asserts_twice + end + + def test_2_child_2_asserts_twice + end + + def test_3_child_2_asserts_twice + end + end + RUBY + end + + def test_does_not_register_offense_if_the_parent_does_not_have_test_methods + assert_no_offenses(<<~RUBY) + class ParentTest < Minitest::Test + end + + class ChildTest < ParentTest + def test_child_asserts_twice + assert_equal(1, 1) + end + end + RUBY + end + + def test_does_not_register_offense_if_the_child_does_not_have_test_methods + assert_no_offenses(<<~RUBY) + class ParentTest < Minitest::Test + def test_child_asserts_twice + assert_equal(1, 1) + end + end + + class ChildTest < ParentTest + end + RUBY + end + + def test_does_not_register_offense_if_the_class_has_no_children + assert_no_offenses(<<~RUBY) + class ParentTest < Minitest::Test + def test_child_asserts_twice + assert_equal(1, 1) + end + end + + class ClassTwo < ParentTest + end + RUBY + end + + def test_does_not_register_offense_if_the_class_is_not_a_test_class + assert_no_offenses(<<~RUBY) + class ParentTest < ExampleClass + def test_child_asserts_twice + assert_equal(1, 1) + end + end + + class ChildClass < ParentTest + def test_child_asserts_twice + assert_equal(1, 1) + end + end + RUBY + end +end