Skip to content

Commit

Permalink
Add new Minitest/UselessAssertion cop
Browse files Browse the repository at this point in the history
  • Loading branch information
fatkodima committed Dec 23, 2022
1 parent b97ae78 commit 8f60f45
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 1 deletion.
1 change: 1 addition & 0 deletions changelog/new_useless_assertion_cop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* [#205](https://github.com/rubocop/rubocop-minitest/issues/205): Add new `Minitest/UselessAssertion` cop. ([@fatkodima][])
5 changes: 5 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,8 @@ Minitest/UnspecifiedException:
StyleGuide: 'https://minitest.rubystyle.guide#unspecified-exception'
Enabled: 'pending'
VersionAdded: '0.10'

Minitest/UselessAssertion:
Description: 'Detects useless assertions (assertions that either always pass or always fail).'
Enabled: pending
VersionAdded: '<<next>>'
75 changes: 75 additions & 0 deletions lib/rubocop/cop/minitest/useless_assertion.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Minitest
# Detects useless assertions (assertions that either always pass or always fail).
#
# @example
# # bad
# assert true
# assert_equal @foo, @foo
# assert_nil [foo, bar]
#
# # good
# assert something
# assert_equal foo, bar
# assert_nil foo
# assert false, "My message"
#
class UselessAssertion < Base
MSG = 'Useless assertion detected.'

SINGLE_ASSERTION_ARGUMENT_METHODS = %i[
assert refute assert_nil refute_nil assert_not assert_empty refute_empty
].freeze
TWO_ASSERTION_ARGUMENTS_METHODS = %i[
assert_equal refute_equal assert_in_delta refute_in_delta
assert_in_epsilon refute_in_epsilon assert_same refute_same
].freeze

RESTRICT_ON_SEND = SINGLE_ASSERTION_ARGUMENT_METHODS +
TWO_ASSERTION_ARGUMENTS_METHODS +
%i[assert_includes refute_includes assert_silent]

def on_send(node)
return if node.receiver

add_offense(node) if offense?(node)
end

private

# rubocop:disable Metrics
def offense?(node)
expected, actual, = node.arguments

case node.method_name
when *SINGLE_ASSERTION_ARGUMENT_METHODS
actual.nil? && expected&.literal?
when *TWO_ASSERTION_ARGUMENTS_METHODS
return false unless expected || actual
return false if expected.source != actual.source

(expected.variable? && actual.variable?) ||
(empty_composite?(expected) && empty_composite?(actual))
when :assert_includes, :refute_includes
expected && empty_composite?(expected)
when :assert_silent
block_node = node.parent
block_node&.body.nil?
else
false
end
end
# rubocop:enable Metrics

def empty_composite?(node)
return true if node.str_type? && node.value.empty?

(node.array_type? || node.hash_type?) && node.children.empty?
end
end
end
end
end
1 change: 1 addition & 0 deletions lib/rubocop/cop/minitest_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@
require_relative 'minitest/test_method_name'
require_relative 'minitest/unreachable_assertion'
require_relative 'minitest/unspecified_exception'
require_relative 'minitest/useless_assertion'
16 changes: 15 additions & 1 deletion lib/rubocop/minitest/assert_offense.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ module Minitest
# RUBY
#
# assert_no_corrections
#
# rubocop:disable Metrics/ModuleLength
module AssertOffense
private

Expand All @@ -79,6 +81,16 @@ def setup
@cop = RuboCop::Cop::Minitest.const_get(cop_name).new
end

def format_offense(source, **replacements)
replacements.each do |keyword, value|
value = value.to_s
source = source.gsub("%{#{keyword}}", value)
.gsub("^{#{keyword}}", '^' * value.size)
.gsub("_{#{keyword}}", ' ' * value.size)
end
source
end

def assert_no_offenses(source, file = nil)
setup_assertion

Expand All @@ -90,11 +102,12 @@ def assert_no_offenses(source, file = nil)
assert_equal(source, actual_annotations.to_s)
end

def assert_offense(source, file = nil)
def assert_offense(source, file = nil, **replacements)
setup_assertion

@cop.instance_variable_get(:@options)[:autocorrect] = true

source = format_offense(source, **replacements)
expected_annotations = RuboCop::RSpec::ExpectOffense::AnnotatedSource.parse(source)
if expected_annotations.plain_source == source
raise 'Use `assert_no_offenses` to assert that no offenses are found'
Expand Down Expand Up @@ -205,5 +218,6 @@ def ruby_version
2.6
end
end
# rubocop:enable Metrics/ModuleLength
end
end
146 changes: 146 additions & 0 deletions test/rubocop/cop/minitest/useless_assertion_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# frozen_string_literal: true

require 'test_helper'

class UselessAssertionTest < Minitest::Test
%i[assert refute assert_nil refute_nil assert_not assert_empty refute_empty].each do |matcher|
define_method("test_#{matcher}_registers_offense_when_using_literals") do
assert_offense(<<~RUBY, matcher: matcher)
#{matcher} []
^{matcher}^^^ Useless assertion detected.
#{matcher} [foo]
^{matcher}^^^^^^ Useless assertion detected.
#{matcher}({})
^{matcher}^^^^ Useless assertion detected.
#{matcher}({ key: value })
^{matcher}^^^^^^^^^^^^^^^^ Useless assertion detected.
#{matcher} nil
^{matcher}^^^^ Useless assertion detected.
#{matcher} true
^{matcher}^^^^^ Useless assertion detected.
#{matcher} false
^{matcher}^^^^^^ Useless assertion detected.
#{matcher} ""
^{matcher}^^^ Useless assertion detected.
#{matcher} "foo"
^{matcher}^^^^^^ Useless assertion detected.
#{matcher} 1
^{matcher}^^ Useless assertion detected.
RUBY
end

define_method("test_#{matcher}_no_offenses_when_using_method_call") do
assert_no_offenses(<<~RUBY)
#{matcher} foo
RUBY
end

define_method("test_#{matcher}_no_offenses_when_using_explicit_message") do
assert_no_offenses(<<~RUBY)
#{matcher} false, "My message"
RUBY
end
end

%i[
assert_equal refute_equal
assert_in_delta refute_in_delta
assert_in_epsilon refute_in_epsilon
assert_same refute_same
].each do |matcher|
define_method("test_#{matcher}_registers_offense_when_using_same_expected_and_actual") do
assert_offense(<<~RUBY, matcher: matcher)
#{matcher} [], []
^{matcher}^^^^^^^ Useless assertion detected.
#{matcher} $foo, $foo
^{matcher}^^^^^^^^^^^ Useless assertion detected.
RUBY
end

define_method("test_#{matcher}_registers_offense_when_using_same_local_variable") do
assert_offense(<<~RUBY, matcher: matcher)
foo = get_foo
#{matcher} foo, foo
^{matcher}^^^^^^^^^ Useless assertion detected.
RUBY
end

define_method("test_#{matcher}_no_offenses_when_different_expected_and_actual") do
assert_no_offenses(<<~RUBY)
#{matcher} foo, bar
RUBY
end

define_method("test_#{matcher}_no_offenses_when_expected_and_actual_are_same_method_call") do
assert_no_offenses(<<~RUBY)
#{matcher} foo, foo
RUBY
end

define_method("test_#{matcher}_no_offenses_when_expected_and_actual_are_same_expression") do
assert_no_offenses(<<~RUBY)
#{matcher} x.foo, x.foo
RUBY
end
end

%i[assert_includes refute_includes].each do |matcher|
define_method("test_#{matcher}_registers_offense_when_using_empty_hash") do
assert_offense(<<~RUBY, matcher: matcher)
#{matcher}({}, foo)
^{matcher}^^^^^^^^^ Useless assertion detected.
RUBY
end

define_method("test_#{matcher}_registers_offense_when_using_empty_array") do
assert_offense(<<~RUBY, matcher: matcher)
#{matcher} [], foo
^{matcher}^^^^^^^^ Useless assertion detected.
RUBY
end

define_method("test_#{matcher}_registers_offense_when_using_empty_string") do
assert_offense(<<~RUBY, matcher: matcher)
#{matcher} "", foo
^{matcher}^^^^^^^^ Useless assertion detected.
RUBY
end

define_method("test_#{matcher}_no_offenses_when_using_non_empty_composite_literal") do
assert_no_offenses(<<~RUBY)
#{matcher} foo, bar
RUBY
end

define_method("test_#{matcher}_no_offenses_when_using_method_call") do
assert_no_offenses(<<~RUBY)
#{matcher} foo, bar
RUBY
end
end

def test_assert_silent_registers_offense_when_block_is_empty
assert_offense(<<~RUBY)
assert_silent {}
^^^^^^^^^^^^^ Useless assertion detected.
RUBY
end

def test_assert_silent_no_offenses_when_block_has_a_body
assert_no_offenses(<<~RUBY)
assert_silent do
foo
end
RUBY
end

def test_ignores_useless_assertion_called_on_receiver
assert_no_offenses(<<~RUBY)
foo.assert(true)
RUBY
end
end

0 comments on commit 8f60f45

Please sign in to comment.