diff --git a/.rubocop.yml b/.rubocop.yml index 2a1f3fd35..e3a7abdb5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,7 @@ require: rubocop-rspec +inherit_from: .rubocop_todo.yml + AllCops: DisplayCopNames: true TargetRubyVersion: 2.2 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 000000000..5df2b585a --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,11 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2016-08-05 22:48:55 -0700 using RuboCop version 0.42.0. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 8 +RSpec/MultipleExpectations: + Max: 3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3465ca21a..e97713484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ * Add `NestedGroups` cop for detecting excessive example group nesting. ([@backus][]) * Add `MaxNesting` configuration option for `NestedGroups` cop. ([@backus][]) * Add `ExpectActual` cop for detecting literal values within `expect(...)`. ([@backus][]) +* Add `MultipleExpectations` cop for detecting multiple `expect(...)` calls within one example. ([@backus][]) +* Add `Max` configuration option for `MultipleExpectations`. ([@backus][]) ## 1.6.0 (2016-08-03) diff --git a/config/default.yml b/config/default.yml index 059cceded..40331c7b3 100644 --- a/config/default.yml +++ b/config/default.yml @@ -37,6 +37,11 @@ RSpec/MultipleDescribes: Description: 'Checks for multiple top level describes.' Enabled: true +RSpec/MultipleExpectations: + Description: 'Checks for multiple `expect(...)` calls in one example.' + Enabled: true + Max: 1 + RSpec/NestedGroups: Description: 'Checks for multiple levels of context nesting.' Enabled: true diff --git a/lib/rubocop-rspec.rb b/lib/rubocop-rspec.rb index ea4967d23..6ce5e97ea 100644 --- a/lib/rubocop-rspec.rb +++ b/lib/rubocop-rspec.rb @@ -22,6 +22,7 @@ require 'rubocop/cop/rspec/focus' require 'rubocop/cop/rspec/instance_variable' require 'rubocop/cop/rspec/multiple_describes' +require 'rubocop/cop/rspec/multiple_expectations' require 'rubocop/cop/rspec/named_subject' require 'rubocop/cop/rspec/nested_groups' require 'rubocop/cop/rspec/not_to_not' diff --git a/lib/rubocop/cop/rspec/multiple_expectations.rb b/lib/rubocop/cop/rspec/multiple_expectations.rb new file mode 100644 index 000000000..4979459e9 --- /dev/null +++ b/lib/rubocop/cop/rspec/multiple_expectations.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Checks if examples contain too many `expect` calls + # + # @see http://betterspecs.org/#single Single expectation test + # + # This cop is configurable using the `Max` option + # and works with `--auto-gen-config`. + # + # @example + # + # # bad + # describe UserCreator do + # it 'builds a user' do + # expect(user.name).to eq("John") + # expect(user.age).to eq(22) + # end + # end + # + # # good + # describe UserCreator do + # it 'sets the users name' do + # expect(user.name).to eq("John") + # end + # + # it 'sets the users age' + # expect(user.age).to eq(22) + # end + # end + # + # @example configuration + # + # # .rubocop.yml + # RSpec/MultipleExpectations: + # Max: 2 + # + # # not flagged by rubocop + # describe UserCreator do + # it 'builds a user' do + # expect(user.name).to eq("John") + # expect(user.age).to eq(22) + # end + # end + # + class MultipleExpectations < Cop + include RuboCop::RSpec::Language, ConfigurableMax + + MSG = 'Too many expectations.'.freeze + + def_node_matcher :example?, <<-PATTERN + (block (send _ {#{Examples::ALL.to_node_pattern}} ...) ...) + PATTERN + + def_node_search :expect, '(send _ :expect ...)' + + def on_block(node) + return unless example?(node) && (expectations = expect(node)) + + return if expectations.count <= max_expectations + + self.max = expectations.count + + flag_example(node, expectation_count: expectations.count) + end + + private + + def flag_example(node, expectation_count:) + method, = *node + + add_offense( + method, + :expression, + MSG % { total: expectation_count, max: max_expectations } + ) + end + + def max_expectations + Integer(cop_config.fetch(parameter_name, 1)) + end + end + end + end +end diff --git a/spec/rubocop/cop/rspec/multiple_expectations_spec.rb b/spec/rubocop/cop/rspec/multiple_expectations_spec.rb new file mode 100644 index 000000000..be923b13a --- /dev/null +++ b/spec/rubocop/cop/rspec/multiple_expectations_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +describe RuboCop::Cop::RSpec::MultipleExpectations, :config do + subject(:cop) { described_class.new(config) } + + context 'without configuration' do + let(:cop_config) { Hash.new } + + it 'flags multiple expectations' do + expect_violation(<<-RUBY) + describe Foo do + it 'uses expect twice' do + ^^^^^^^^^^^^^^^^^^^^^^ Too many expectations. + expect(foo).to eq(bar) + expect(baz).to eq(bar) + end + end + RUBY + end + + it 'approves of one expectation per example' do + expect_no_violations(<<-RUBY) + describe Foo do + it 'does something neat' do + expect(neat).to be(true) + end + + it 'does something cool' do + expect(cool).to be(true) + end + end + RUBY + end + end + + context 'with configuration' do + let(:cop_config) do + { 'Max' => '2' } + end + + it 'permits two expectations' do + expect_no_violations(<<-RUBY) + describe Foo do + it 'uses expect twice' do + expect(foo).to eq(bar) + expect(baz).to eq(bar) + end + end + RUBY + end + + it 'flags three expectations' do + expect_violation(<<-RUBY) + describe Foo do + it 'uses expect three times' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Too many expectations. + expect(foo).to eq(bar) + expect(baz).to eq(bar) + expect(qux).to eq(bar) + end + end + RUBY + end + end + + it 'generates a todo based on the worst violation' do + inspect_source(cop, <<-RUBY) + describe Foo do + it 'uses expect twice' do + expect(foo).to eq(bar) + expect(baz).to eq(bar) + end + + it 'uses expect three times' do + expect(foo).to eq(bar) + expect(baz).to eq(bar) + expect(qux).to eq(bar) + end + end + RUBY + + expect(cop.config_to_allow_offenses).to eq('Max' => 3) + end +end