Skip to content

Commit

Permalink
Merge pull request #88 from fatkodima/multiple-assertions-cop
Browse files Browse the repository at this point in the history
Add new `Minitest/MultipleAssertions` cop
  • Loading branch information
koic committed Jul 2, 2020
2 parents 59bac61 + 3880f0d commit 28dbd0c
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### New features

* [#83](https://github.com/rubocop-hq/rubocop-minitest/pull/83): New cops `AssertPathExists` and `RefutePathExists` check for use of `assert_path_exists`/`refute_path_exists` instead of `assert(File.exist?(path))`/`refute(File.exist?(path))`. ([@fatkodima][])
* [#88](https://github.com/rubocop-hq/rubocop-minitest/pull/88): Add new `Minitest/MultipleAssertions` cop. ([@fatkodima][])
* [#87](https://github.com/rubocop-hq/rubocop-minitest/pull/87): Add new `Minitest/AssertSilent` cop. ([@fatkodima][])
* [#84](https://github.com/rubocop-hq/rubocop-minitest/pull/84): New cops `AssertKindOf` and `RefuteKindOf` check for use of `assert_kind_of`/`refute_kind_of` instead of `assert(foo.kind_of?(Class))`/`refute(foo.kind_of?(Class))`. ([@fatkodima][])
* [#85](https://github.com/rubocop-hq/rubocop-minitest/pull/85): Add autocorrect to `Rails/AssertEmptyLiteral` cop. ([@fatkodima][])
Expand Down
6 changes: 6 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ Minitest/GlobalExpectations:
Enabled: true
VersionAdded: '0.7'

Minitest/MultipleAssertions:
Description: 'This cop checks if test cases contain too many assertion calls.'
Enabled: 'pending'
VersionAdded: '0.10'
Max: 3

Minitest/RefuteEmpty:
Description: 'This cop enforces to use `refute_empty` instead of using `refute(object.empty?)`.'
StyleGuide: 'https://minitest.rubystyle.guide#refute-empty'
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* xref:cops_minitest.adoc#minitestassertsilent[Minitest/AssertSilent]
* xref:cops_minitest.adoc#minitestasserttruthy[Minitest/AssertTruthy]
* xref:cops_minitest.adoc#minitestglobalexpectations[Minitest/GlobalExpectations]
* xref:cops_minitest.adoc#minitestmultipleassertions[Minitest/MultipleAssertions]
* xref:cops_minitest.adoc#minitestrefuteempty[Minitest/RefuteEmpty]
* xref:cops_minitest.adoc#minitestrefuteequal[Minitest/RefuteEqual]
* xref:cops_minitest.adoc#minitestrefutefalse[Minitest/RefuteFalse]
Expand Down
51 changes: 51 additions & 0 deletions docs/modules/ROOT/pages/cops_minitest.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,57 @@ _ { musts }.must_raise TypeError

* https://minitest.rubystyle.guide#global-expectations

== Minitest/MultipleAssertions

|===
| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged

| Pending
| Yes
| No
| 0.10
| -
|===

This cop checks if test cases contain too many assertion calls.
The maximum allowed assertion calls is configurable.

=== Examples

==== Max: 1

[source,ruby]
----
# bad
class FooTest < Minitest::Test
def test_asserts_twice
assert_equal(42, do_something)
assert_empty(array)
end
end
# good
class FooTest < Minitest::Test
def test_asserts_once
assert_equal(42, do_something)
end
def test_another_asserts_once
assert_empty(array)
end
end
----

=== Configurable attributes

|===
| Name | Default value | Configurable values

| Max
| `3`
| Integer
|===

== Minitest/RefuteEmpty

|===
Expand Down
63 changes: 63 additions & 0 deletions lib/rubocop/cop/minitest/multiple_assertions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Minitest
# This cop checks if test cases contain too many assertion calls.
# The maximum allowed assertion calls is configurable.
#
# @example Max: 1
# # bad
# class FooTest < Minitest::Test
# def test_asserts_twice
# assert_equal(42, do_something)
# assert_empty(array)
# end
# end
#
# # good
# class FooTest < Minitest::Test
# def test_asserts_once
# assert_equal(42, do_something)
# end
#
# def test_another_asserts_once
# assert_empty(array)
# end
# end
#
class MultipleAssertions < Cop
include ConfigurableMax
include MinitestExplorationHelpers

MSG = 'Test case has too many assertions [%<total>d/%<max>d].'

def on_class(class_node)
return unless minitest_test_subclass?(class_node)

test_cases(class_node).each do |node|
assertions_count = assertions_count(node)

next unless assertions_count > max_assertions

self.max = assertions_count

message = format(MSG, total: assertions_count, max: max_assertions)
add_offense(node, location: :name, message: message)
end
end

private

def assertions_count(node)
base = assertion?(node) ? 1 : 0
base + node.each_child_node.sum { |c| assertions_count(c) }
end

def max_assertions
Integer(cop_config.fetch('Max', 3))
end
end
end
end
end
2 changes: 2 additions & 0 deletions lib/rubocop/cop/minitest_cops.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative 'mixin/argument_range_helper'
require_relative 'mixin/minitest_cop_rule'
require_relative 'mixin/minitest_exploration_helpers'
require_relative 'minitest/assert_empty'
require_relative 'minitest/assert_empty_literal'
require_relative 'minitest/assert_equal'
Expand All @@ -15,6 +16,7 @@
require_relative 'minitest/assert_silent'
require_relative 'minitest/assert_truthy'
require_relative 'minitest/global_expectations'
require_relative 'minitest/multiple_assertions'
require_relative 'minitest/refute_empty'
require_relative 'minitest/refute_false'
require_relative 'minitest/refute_equal'
Expand Down
79 changes: 79 additions & 0 deletions lib/rubocop/cop/mixin/minitest_exploration_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

require 'set'

module RuboCop
module Cop
# Helper methods for different explorations against test files and test cases.
module MinitestExplorationHelpers
extend NodePattern::Macros

ASSERTIONS = %i[
assert
assert_empty
assert_equal
assert_in_delta
assert_in_epsilon
assert_includes
assert_instance_of
assert_kind_of
assert_match
assert_mock
assert_nil
assert_operator
assert_output
assert_path_exists
assert_predicate
assert_raises
assert_respond_to
assert_same
assert_send
assert_silent
assert_throws
refute
refute_empty
refute_equal
refute_in_delta
refute_in_epsilon
refute_includes
refute_instance_of
refute_kind_of
refute_match
refute_nil
refute_operator
refute_path_exists
refute_predicate
refute_respond_to
refute_same
].to_set.freeze

private

def minitest_test_subclass?(class_node)
minitest_test?(class_node.parent_class)
end

def_node_matcher :minitest_test?, <<~PATTERN
(const (const nil? :Minitest) :Test)
PATTERN

def test_cases(class_node)
class_def = class_node.body
return [] unless class_def

def_nodes =
if class_def.def_type?
[class_def]
else
class_def.each_child_node(:def)
end

def_nodes.select { |c| c.method_name.to_s.start_with?('test_') }
end

def assertion?(node)
node.send_type? && ASSERTIONS.include?(node.method_name)
end
end
end
end
81 changes: 81 additions & 0 deletions test/rubocop/cop/minitest/multiple_assertions_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true

require 'test_helper'

class MultipleAssertionsTest < Minitest::Test
def setup
configure_max_assertions(1)
end

def test_registers_offense_when_multiple_expectations
assert_offense(<<~RUBY)
class FooTest < Minitest::Test
def test_asserts_twice
^^^^^^^^^^^^^^^^^^ Test case has too many assertions [2/1].
assert_equal(foo, bar)
assert_empty(array)
end
end
RUBY
end

def test_checks_only_minitest_test_children
assert_no_offenses(<<~RUBY)
class FooTest
def test_asserts_twice
assert_equal(foo, bar)
assert_empty(array)
end
end
RUBY
end

def test_checks_only_test_case_methods
assert_no_offenses(<<~RUBY)
class FooTest < Minitest::Test
# No 'test_' prefix
def asserts_twice
assert_equal(foo, bar)
assert_empty(array)
end
end
RUBY
end

def test_does_not_register_offense_when_single_assertion
assert_no_offenses(<<~RUBY)
class FooTest < Minitest::Test
def test_asserts_once
assert_equal(foo, bar)
end
end
RUBY
end

def test_generates_a_todo_based_on_the_worst_violation
inspect_source(<<-RUBY, @cop, 'test/foo_test.rb')
class FooTest < Minitest::Test
def test_asserts_once
assert_equal(foo, bar)
assert_equal(baz, bar)
end
def test_asserts_two_times
assert_equal(foo, bar)
assert_equal(baz, bar)
end
end
RUBY

assert_equal({ 'Max' => 2 }, @cop.config_to_allow_offenses[:exclude_limit])
end

private

def configure_max_assertions(max)
cop_config = RuboCop::Config.new('Minitest/MultipleAssertions' => {
'Max' => max
})
@cop = RuboCop::Cop::Minitest::MultipleAssertions.new(cop_config)
end
end

0 comments on commit 28dbd0c

Please sign in to comment.