Class: RuboCop::Cop::DevDoc::Test::NoUnitIdiomsInIntegrationTests
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Test::NoUnitIdiomsInIntegrationTests
- Defined in:
- lib/rubocop/cop/dev_doc/test/no_unit_idioms_in_integration_tests.rb
Overview
Integration tests must be BOUNDARY tests: exercise one entry point (an HTTP request, or a job for cron-driven flows with no HTTP surface) and assert only its observable outputs — response status and body, database state, delivered/enqueued mail. Unit-test idioms under the integration base class are dishonest by construction: the base class is a declaration of WHAT KIND of test this is, and the runtime/static lints trust that declaration.
What flags, and why
@controller,assigns,instance_variable_get: reaching into request-internal state the boundary is supposed to hide.FooService.new/FooPolicy.new(any suffix inUnitClassSuffixes): instantiating app-layer objects to test or query them directly. When used to COMPUTE an expected value, this is a circular assertion — the test asks production code what to expect, so a bug in that code passes its own test. Pin literals or fixture facts instead.FooMailer.with(...)and any other direct mailer invocation: manufacture mail through the flow that actually sends it, then readdeliveries.include FooHelper: importing production helpers to compute expectations — circular, same as above (*TestHelpermodules are exempt).Minitest::Mock,.stub(...),require 'minitest/mock': mock/stub machinery is unit-test tooling by definition.
What deliberately does NOT flag
- Model reads (
Organization.last,Member.find_by!, counts, attribute readers): the database is an OUTBOUND edge of the boundary — asserting persisted state is the point — and fixture lookup is setup. - Job classes (
FooJob.new,perform_now,perform_enqueued_jobs): jobs are an INBOUND edge — the entry point for cron flows.
The honesty rule
If the property cannot be observed at any boundary, the test is a
unit test. Declare it: Glib::LastResortUnitTest in an exempt
directory with a justification header. An observed evasion (2026-07,
a guard-aware agent bolting a GET onto direct policy assertions)
is why this cop assumes disguise rather than accident. Do not
disable this cop.
Constant Summary collapse
- MSG =
'`%<ref>s` is a unit-test idiom inside an integration test. A boundary test ' \ 'exercises one entry point (HTTP request, or a job for cron flows) and asserts ' \ 'only its observable outputs (status, body, DB state, mails). Computing an ' \ 'expectation with production code is a CIRCULAR assertion (the code grades its ' \ 'own homework); instantiating app objects or reaching into the controller is ' \ 'unit testing in disguise, and a bolted-on request does not change what it is. ' \ 'Be honest about what this test is: assert through the boundary, or declare a ' \ 'real unit test (Glib::LastResortUnitTest + exempt directory + justification ' \ 'header). Do not disable this cop.'.freeze
- CONTROLLER_INTERNALS =
%i[assigns instance_variable_get].freeze
- HELPER_EXEMPT =
/TestHelper\z|MailerTester\z/- MOCK_REQUIRES =
['minitest/mock'].freeze
- DEFAULT_UNIT_CLASS_SUFFIXES =
Baseline lives in code (config lists are droppable; see CurrentUserBranching's DEFAULT_ALLOWED_METHODS for the incident that taught this). The yml key ADDS suffixes, it cannot remove these.
%w[Service Policy Form Presenter Serializer Decorator].freeze
Instance Method Summary collapse
Instance Method Details
#on_ivar(node) ⇒ Object
67 68 69 70 71 |
# File 'lib/rubocop/cop/dev_doc/test/no_unit_idioms_in_integration_tests.rb', line 67 def on_ivar(node) return unless node.children.first == :@controller add_offense(node, message: format(MSG, ref: '@controller')) end |
#on_send(node) ⇒ Object
73 74 75 76 77 78 |
# File 'lib/rubocop/cop/dev_doc/test/no_unit_idioms_in_integration_tests.rb', line 73 def on_send(node) ref = offense_ref(node) return unless ref add_offense(node, message: format(MSG, ref: ref)) end |