Skip to content

Commit

Permalink
Merge pull request #160 from nevir/feature/MultipleExpectations
Browse files Browse the repository at this point in the history
Add `RSpec/MultipleExpectations` cop
  • Loading branch information
backus authored Aug 6, 2016
2 parents 08b4b93 + 30bb839 commit a0c451c
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require: rubocop-rspec

inherit_from: .rubocop_todo.yml

AllCops:
DisplayCopNames: true
TargetRubyVersion: 2.2
Expand Down
11 changes: 11 additions & 0 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
5 changes: 5 additions & 0 deletions config/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop-rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
87 changes: 87 additions & 0 deletions lib/rubocop/cop/rspec/multiple_expectations.rb
Original file line number Diff line number Diff line change
@@ -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
84 changes: 84 additions & 0 deletions spec/rubocop/cop/rspec/multiple_expectations_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit a0c451c

Please sign in to comment.