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