Class: RuboCop::Cop::DevDoc::Test::ResponseAssertEqual

Inherits:
Base
  • Object
show all
Defined in:
lib/rubocop/cop/dev_doc/test/response_assert_equal.rb

Overview

Controller tests that assert on the rendered ‘response.body` should also snapshot the full response with `response_assert_equal`.

## Rationale ‘response_assert_equal` (glib-web’s ‘Glib::TestHelpers`) is the most blackbox assertion available — it snapshots the entire rendered response, so it catches regressions anywhere in the output, not just the one substring a targeted assertion happens to check. Per the testing best practice, happy- and unhappy-path controller tests should always use it.

The common slip this cop guards against: a test inspects the rendered body with ‘assert_match` / `assert_no_match` / `assert_includes` / `response.body.scan(…)` but forgets the snapshot. The targeted assertion documents intent, but on its own it only proves the one line — collateral changes elsewhere in the response slip through.

❌ Inspects the body, but no snapshot
test 'index lists the item' do
  get items_url(format: :json)
  assert_response :success
  assert_match 'Widget', response.body
end

✔️ Keep the focused assertion AND snapshot the whole response
test 'index lists the item' do
  get items_url(format: :json)
  assert_response :success
  assert_match 'Widget', response.body
  response_assert_equal
end

## What is NOT flagged

  • Tests that already call ‘response_assert_equal`.

  • Exception/redirect paths — a test asserting a non-success status (‘assert_response :not_found`, `:forbidden`, `:redirect`, etc.) often has no stable rendered body to snapshot.

  • State-change tests that never read ‘response.body` (e.g. `assert_difference ’Model.count’‘, mailer assertions). Passing `response.body` to a helper like `submit_form(response.body, …)` is not a body assertion and is not flagged.

## Inline disable For the rare happy-path test whose response is genuinely non-deterministic and cannot be made stable, add a ‘rubocop:disable DevDoc/Test/ResponseAssertEqual` comment to the `test ’…‘ do` line, with a written reason on the next line (e.g. “chunked response includes a per-run boundary token”).

Examples:

# bad
test 'shows the row' do
  get foo_url(format: :json)
  assert_match 'bar', response.body
end

# good
test 'shows the row' do
  get foo_url(format: :json)
  assert_match 'bar', response.body
  response_assert_equal
end

Constant Summary collapse

MSG =
'Asserts on `response.body` but never calls `response_assert_equal`. ' \
'Add the snapshot assertion (usually last) — it captures the whole rendered ' \
'response, a stronger guard than a targeted body assertion. Exception-path ' \
'tests that cannot snapshot may disable this cop with a reason.'.freeze
NON_SUCCESS_STATUS_SYMBOLS =

Non-success HTTP statuses (symbol form). A test asserting any of these is an exception / redirect / empty-body path — no JSON body to snapshot.

%i[
  not_found forbidden unauthorized unprocessable_entity unprocessable_content
  bad_request conflict gone no_content not_modified redirect moved_permanently
  found see_other payment_required too_many_requests precondition_failed
].freeze

Instance Method Summary collapse

Instance Method Details

#calls_snapshot?(node) ⇒ Object



84
85
86
# File 'lib/rubocop/cop/dev_doc/test/response_assert_equal.rb', line 84

def_node_search :calls_snapshot?, <<~PATTERN
  (send nil? :response_assert_equal)
PATTERN

#on_block(node) ⇒ Object



100
101
102
103
104
105
106
107
108
# File 'lib/rubocop/cop/dev_doc/test/response_assert_equal.rb', line 100

def on_block(node)
  return unless test_block?(node)
  return unless node.body
  return if calls_snapshot?(node)
  return if exempt_from_snapshot?(node)
  return unless inspects_response_body?(node)

  add_offense(node.send_node.loc.selector)
end

#response_body_read?(node) ⇒ Object



79
80
81
# File 'lib/rubocop/cop/dev_doc/test/response_assert_equal.rb', line 79

def_node_matcher :response_body_read?, <<~PATTERN
  (send (send nil? :response) {:body :parsed_body})
PATTERN

#response_status?(node) ⇒ Object



89
# File 'lib/rubocop/cop/dev_doc/test/response_assert_equal.rb', line 89

def_node_matcher :response_status?, '(send (send nil? :response) :status)'

#test_block?(node) ⇒ Object



74
75
76
# File 'lib/rubocop/cop/dev_doc/test/response_assert_equal.rb', line 74

def_node_matcher :test_block?, <<~PATTERN
  (block (send nil? :test ...) _args _body)
PATTERN