Class: RuboCop::Cop::DevDoc::Test::AvoidUnitTest
- Inherits:
-
Base
- Object
- Base
- RuboCop::Cop::DevDoc::Test::AvoidUnitTest
- Defined in:
- lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb
Overview
Prefer controller tests; flag unit/service tests (‘< ActiveSupport::TestCase`).
## Rationale What the user sees and experiences is what matters; internal implementation does not. A controller test exercises behaviour end-to-end through the same path a user takes, so it catches the regressions that actually reach production. A unit/service test is only needed when a code path genuinely cannot be reached through a controller test (very rare) — e.g. a search-ranking detail the controller never exposes.
The danger a unit test hides: it can stay green while the feature is broken in production. It proves a method works in isolation — NOT that the real request path calls that method, in the right order, inside the transaction it needs. A model method can be flawless in a unit test while the controller calls the wrong method, skips it, or runs it outside its transaction; the unit test stays green and the feature is broken. Controller tests fail when the wiring is wrong — which is where regressions actually live. So a passing unit test is not evidence the feature works; it is false confidence about production. Reach for one only when you are sure a controller test genuinely cannot reach the path, not because it is quicker to write.
This cop flags only the literal ‘< ActiveSupport::TestCase` superclass. The blessed blackbox bases —`ActionDispatch::IntegrationTest`, `Glib::IntegrationTest`, `ActionMailer::TestCase`, `ActiveJob::TestCase` — are NOT flagged, even though they inherit from `ActiveSupport::TestCase` transitively.
## Escape hatch Before reaching for a unit test, assume a controller test IS possible and look harder — that conclusion is almost always premature. Behaviour that feels inherently unit-level is usually reachable end-to-end:
- Transaction rollback / "a failure mid-request": inject the failure
at a class-method chokepoint the gem/service calls (stub it to
raise), drive the real request, and assert the observable rollback
(e.g. `assert_no_difference` on the record count). Even atomicity,
which feels inherently unit-level, is reachable this way.
- "The controller wraps it in a transaction so I can't isolate the
model's own": you usually don't need to — assert the *observable*
guarantee through the real path; that is what matters in production.
When a unit test is genuinely necessary, suppress with a reason that explains why a controller test can’t cover the path. That reason IS the required justification — keep it specific and reviewable:
# rubocop:disable DevDoc/Test/AvoidUnitTest -- search ranking isn't visible through the controller
class Ai::Retrieval::PgSearchStrategyTest < ActiveSupport::TestCase
# ...
end
# rubocop:enable DevDoc/Test/AvoidUnitTest
NOTE: The cop matches the direct superclass only. A project base (‘class ApplicationServiceTest < ActiveSupport::TestCase`) is flagged once (justify it there); subclasses of that base are not re-flagged.
Constant Summary collapse
- MSG =
'Prefer a controller test — unit tests are a rare exception. If a controller test ' \ 'genuinely cannot cover this path, disable this cop on the class with a reason.'.freeze
Instance Method Summary collapse
Instance Method Details
#on_class(node) ⇒ Object
79 80 81 82 83 84 85 |
# File 'lib/rubocop/cop/dev_doc/test/avoid_unit_test.rb', line 79 def on_class(node) superclass = node.parent_class return unless superclass&.const_type? return unless superclass.const_name == 'ActiveSupport::TestCase' add_offense(superclass) end |