Module: DevDoc::Test::Lints::HttpDrivenControllerTests
- Defined in:
- lib/dev_doc/test/lints/http_driven_controller_tests.rb
Overview
Runtime guarantee that every test under test/controllers/ actually
drives the app through HTTP. Include into the project's base
integration test class (a descendant of ActionDispatch::IntegrationTest):
Glib::IntegrationTest.include DevDoc::Test::Lints::HttpDrivenControllerTests
Rationale
Best practice (see AvoidUnitTest) prefers controller tests because they
exercise the wiring a user actually hits. Static enforcement keyed on
base class or file name is trivially evaded — and observed evasions by
AI agents include: naming files ambiguously, mixing unit tests into
controller test files, and inheriting the integration base class so the
< ActiveSupport::TestCase heuristic never fires.
This lint closes those holes by keying on observed behavior:
ActionDispatch::Integration::Runner#process is the single choke point
every request helper funnels through (get/post/... — no matter how
deeply wrapped in log_user_in-style helpers), so "issued at least one
request" is checked per test, not per file. A unit test cannot pass it
by renaming, relocating within the file, or subclassing anything.
There is deliberately NO opt-out flag, and the escape route is deliberately ordered: CONVERT first, relocate only as a last resort. The observed failure mode when this lint fires is relocation-verbatim — the test moves to a unit/side-effect directory unchanged, and the HTTP wiring its subject participates in (a controller param handed to a mailer, a policy branch, a rendered error) silently loses its only prospective coverage. That is why the destination header must carry a "Wiring:" line (enforced by DevDoc/Test/RequireUnitTestJustification): writing it forces the mover to go look for the request test that still covers the wiring — and to notice when there isn't one. Relocated tests must also re-base off the integration class (e.g. onto Glib::LastResortUnitTest or Glib::NonHttpIntegrationTest); the destination dirs forbid integration bases, so a test moved unchanged keeps tripping that cop.
Constant Summary collapse
- CONTROLLER_TEST_PATH =
%r{(\A|/)test/controllers/}- MESSAGE =
"%<location>s lives under test/controllers/ but never issued an HTTP request \u2014\nit is a unit test in controller-test clothing. CONVERT it first: drive the\nsame behavior through a request (the bugs live in the wiring), assuming a\nrequest test IS possible and looking harder \u2014 that conclusion is almost\nalways premature (see DevDoc/Test/AvoidUnitTest for the patterns). Only if\nconversion is genuinely impossible, RELOCATE it to the directory matching\nwhat it exercises (test/models/, test/jobs/, ...) on a non-integration base\n(e.g. Glib::LastResortUnitTest / Glib::NonHttpIntegrationTest) \u2014 never\nverbatim: the destination header must justify the exception AND carry a\n\"Wiring:\" line naming the request test that still covers the HTTP path this\ntest's subject participates in (or stating why no HTTP path exists), both\nenforced by DevDoc/Test/RequireUnitTestJustification. Relocating assertions\ninto the mailer-preview harness only pins the mailer itself \u2014 a request test\nmust still prove the controller feeds it (params -> Mailer.with(...)).\nNEVER bolt on a throwaway request just to silence this lint: a fig-leaf\nGET whose response nothing asserts against is the disguise the companion\ncop (DevDoc/Test/NoUnitIdiomsInIntegrationTests) polices. HTTP is not the\nonly entry point \u2014 cron-driven flows are legitimately exercised by\ninvoking their JOB and asserting its observable outputs.\n".freeze
Instance Method Summary collapse
Instance Method Details
#after_teardown ⇒ Object
81 82 83 84 85 |
# File 'lib/dev_doc/test/lints/http_driven_controller_tests.rb', line 81 def after_teardown super ensure __dev_doc_check_http_driven end |
#before_setup ⇒ Object
64 65 66 67 |
# File 'lib/dev_doc/test/lints/http_driven_controller_tests.rb', line 64 def before_setup @__dev_doc_http_request_count = 0 super end |