class RuboCop::Cop::RSpec::AggregateExamples

Checks if example groups contain two or more aggregatable examples.

@see github.com/rubocop-hq/rspec-style-guide#expectation-per-example

This cop is primarily for reducing the cost of repeated expensive context initialization.

@example

# bad
describe do
  specify do
    expect(number).to be_positive
    expect(number).to be_odd
  end

  it { is_expected.to be_prime }
end

# good
describe do
  specify do
    expect(number).to be_positive
    expect(number).to be_odd
    is_expected.to be_prime
  end
end

# fair - subject has side effects
describe do
  specify do
    expect(multiply_by(2)).to be_multiple_of(2)
  end

  specify do
    expect(multiply_by(3)).to be_multiple_of(3)
  end
end

Block expectation syntax is deliberately not supported due to:

  1. `subject { -> { … } }` syntax being hard to detect, e.g. the following looks like an example with non-block syntax, but it might be, depending on how the subject is defined:

    it { is_expected.to do_something }
    

    If the subject is defined in a `shared_context`, it's impossible to detect that at all.

  2. Aggregation should use composition with an `.and`. Also, aggregation of the `not_to` expectations is barely possible when a matcher doesn't provide a negated variant.

  3. Aggregation of block syntax with non-block syntax should be in a specific order.

RSpec [comes with an `aggregate_failures` helper](relishapp.com/rspec/rspec-expectations/docs/aggregating-failures) not to fail the example on first unmet expectation that might come handy with aggregated examples. It can be [used in metadata form](relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures#use-%60:aggregate-failures%60-metadata), or [enabled globally](relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures#enable-failure-aggregation-globally-using-%60define-derived-metadata%60).

@example Globally enable `aggregate_failures`

# spec/spec_helper.rb
config.define_derived_metadata do |metadata|
  unless metadata.key?(:aggregate_failures)
    metadata[:aggregate_failures] = true
  end
end

To match the style being used in the spec suite, AggregateExamples can be configured to add `:aggregate_failures` metadata to the example or not. The option not to add metadata can be also used when it's not desired to make expectations after previously failed ones, commonly known as fail-fast.

The terms “aggregate examples” and “aggregate failures” not to be confused. The former stands for putting several expectations to a single example. The latter means to run all the expectations in the example instead of aborting on the first one.

@example AddAggregateFailuresMetadata: true (default)

# Metadata set using a symbol
specify(:aggregate_failures) do
  expect(number).to be_positive
  expect(number).to be_odd
end

@example AddAggregateFailuresMetadata: false

specify do
  expect(number).to be_positive
  expect(number).to be_odd
end

Constants

MSG

Public Instance Methods

autocorrect(example_node) click to toggle source
# File lib/test_prof/cops/rspec/aggregate_examples.rb, line 135
def autocorrect(example_node)
  clusters = example_clusters_for_autocorrect(example_node)
  return if clusters.empty?

  lambda do |corrector|
    clusters.each do |metadata, examples|
      range = range_for_replace(examples)
      replacement = aggregated_example(examples, metadata)
      corrector.replace(range, replacement)
      examples[1..-1].map { |example| drop_example(corrector, example) }
    end
  end
end
on_block(node) click to toggle source
# File lib/test_prof/cops/rspec/aggregate_examples.rb, line 123
def on_block(node)
  example_group_with_several_examples(node) do |all_examples|
    example_clusters(all_examples).each do |_, examples|
      examples[1..-1].each do |example|
        add_offense(example,
          location: :expression,
          message: message_for(example, examples[0]))
      end
    end
  end
end

Private Instance Methods

aggregated_example(examples, metadata) click to toggle source
# File lib/test_prof/cops/rspec/aggregate_examples.rb, line 178
def aggregated_example(examples, metadata)
  base_indent = " " * examples.first.source_range.column
  metadata = metadata_for_aggregated_example(metadata)
  [
    "#{base_indent}specify#{metadata} do",
    *examples.map { |example| transform_body(example, base_indent) },
    "#{base_indent}end\n"
  ].join("\n")
end
drop_example(corrector, example) click to toggle source
# File lib/test_prof/cops/rspec/aggregate_examples.rb, line 172
def drop_example(corrector, example)
  aggregated_range = range_by_whole_lines(example.source_range,
    include_final_newline: true)
  corrector.remove(aggregated_range)
end
example_clusters(all_examples) click to toggle source

Clusters of examples in the same example group, on the same nesting level that can be aggregated.

# File lib/test_prof/cops/rspec/aggregate_examples.rb, line 153
def example_clusters(all_examples)
  all_examples
    .select { |example| example_with_expectations_only?(example) }
    .group_by { |example| metadata_without_aggregate_failures(example) }
    .select { |_, examples| examples.count > 1 }
end
example_clusters_for_autocorrect(example_node) click to toggle source

Clusters of examples that can be aggregated without losing any information (e.g. metadata or docstrings)

# File lib/test_prof/cops/rspec/aggregate_examples.rb, line 162
def example_clusters_for_autocorrect(example_node)
  examples_in_group = example_node.parent.each_child_node(:block)
    .select { |example| example_for_autocorrect?(example) }
  example_clusters(examples_in_group)
end
message_for(_example, first_example) click to toggle source
# File lib/test_prof/cops/rspec/aggregate_examples.rb, line 168
def message_for(_example, first_example)
  format(MSG, first_example.loc.line)
end
new_body(node) click to toggle source
# File lib/test_prof/cops/rspec/aggregate_examples.rb, line 193
def new_body(node)
  node.body.source
end
transform_body(node, base_indent) click to toggle source

Extracts and transforms the body, keeping proper indentation.

# File lib/test_prof/cops/rspec/aggregate_examples.rb, line 189
def transform_body(node, base_indent)
  "#{base_indent}  #{new_body(node)}"
end