Skip to content

feat: add http-multiple checker for multiple HTTP assertions against same handler#296

Open
mmorel-35 wants to merge 3 commits into
Antonboom:masterfrom
mmorel-35:issue-193
Open

feat: add http-multiple checker for multiple HTTP assertions against same handler#296
mmorel-35 wants to merge 3 commits into
Antonboom:masterfrom
mmorel-35:issue-193

Conversation

@mmorel-35

@mmorel-35 mmorel-35 commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

Each testify HTTP assertion function (HTTPStatusCode, HTTPBodyContains, etc.) makes a separate HTTP call to the handler. Multiple assertions with identical (handler, method, url, values) arguments allow a stateful handler to satisfy each test independently — masking bugs that only appear in a real single-request scenario.

New checker: http-multiple

Detects when multiple HTTP assertion functions share the same handler and request arguments within a single function scope:

// ❌ Two separate HTTP calls — stateful handler can cheat
assert.HTTPStatusCode(t, handler, "GET", "/path", nil, http.StatusOK)
assert.HTTPBodyContains(t, handler, "GET", "/path", nil, "hello")

// ✅ Single HTTP call, assert multiple response properties
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/path", nil)
w := httptest.NewRecorder()
handler(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "hello")

Changes

  • internal/checkers/http_multiple.goAdvancedChecker implementation: walks each function/closure body (without crossing FuncLit boundaries), groups HTTP assertions by (handler, method, url, values) string key, reports all calls in groups of 2+. Includes a suggested fix that replaces consecutive groups with a scoped httptest block.
  • internal/checkers/helpers_import.go — import-addition utilities (identical copy from the http-const branch) providing addImportFix to resolve a package's local qualifier and optionally insert a missing import as a TextEdit.
  • internal/checkers/checkers_registry.go — registers http-multiple, enabled by default
  • internal/checkers/checkers_registry_test.go — updates expected checker lists
  • internal/testgen/gen_http_multiple.go — test generator covering invalid (same args, including *f formatted variants, non-nil url.Values query params) and valid (single call, different handlers/methods/URLs, cross-closure boundaries) cases; includes a GoldenTemplate showing the expected state after all fixes are applied; covers non-t TestingT identifiers (e.g. tt)
  • internal/testgen/main.go — registers the generator
  • analyzer/checkers_factory_test.go — updates expected advanced checker lists
  • analyzer/testdata/src/http-multiple-add-import/ — separate testdata package (not managed by testgen) exercising the import-addition path end-to-end when net/http/httptest is not yet present
  • README.md — documents the checker with autofix marked as enabled

Autofix

The fix is generated when all calls in a group reside in consecutive top-level statements of the enclosing block and every call is the direct expression of its ast.ExprStmt (not nested inside if/for/select bodies). It replaces them with a scoped { } block:

{
    req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/path", nil)
    rr := httptest.NewRecorder()
    handler(rr, req)
    assert.Equal(t, http.StatusOK, rr.Code)
    assert.Contains(t, rr.Body.String(), "hello")
}

t.Context() is used (where t is the actual TestingT variable extracted from the original call) so the request context is automatically cancelled when the test ends — without requiring an explicit "context" import. The block wrapper ensures that applying fixes for multiple groups in the same function at once does not produce variable re-declaration errors. The following assertion mappings are used:

HTTP assertion Replacement
HTTPStatusCode(..., code) assert.Equal(<t>, code, rr.Code)
HTTPBodyContains(..., str) assert.Contains(<t>, rr.Body.String(), str)
HTTPBodyNotContains(..., str) assert.NotContains(<t>, rr.Body.String(), str)
HTTPError(...) assert.GreaterOrEqual(<t>, rr.Code, 400)
HTTPSuccess(...) assert.GreaterOrEqual(<t>, rr.Code, 200) + assert.Less(<t>, rr.Code, 300)
HTTPRedirect(...) assert.GreaterOrEqual(<t>, rr.Code, 300) + assert.Less(<t>, rr.Code, 400)

Where <t> is the actual TestingT variable name extracted from each original call (e.g. tt if the test uses tt *testing.T).

Additional fix safety constraints:

  • Non-nested only: fix is skipped when any call is nested inside if/for/select/etc. — prevents replacing surrounding control flow.
  • Consistent package: fix is skipped when the group mixes assert.* and require.* calls — prevents silent semantic changes.
  • Import handling: uses addImportFix (from helpers_import.go) to resolve the httptest qualifier and automatically add the net/http/httptest import when not yet present in the file. No "context" import is needed since t.Context() is used. The fix is offered unconditionally.
  • url.Values encoding: when values is non-nil, the fix emits req.URL.RawQuery = <values>.Encode(), mirroring testify's own httpCode implementation.

*f formatted variants are preserved in the replacement. Non-consecutive groups and groups that fail any safety constraint still receive a diagnostic but no fix.

Closes #193

…same handler

Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
@mmorel-35 mmorel-35 force-pushed the issue-193 branch 4 times, most recently from 450f5c7 to 196df69 Compare March 14, 2026 09:46
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
Copilot AI added a commit to mmorel-35/testifylint that referenced this pull request May 28, 2026
mmorel-35 pushed a commit to mmorel-35/testifylint that referenced this pull request May 28, 2026
@Antonboom Antonboom added the llm-based LLM shit label Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

llm-based LLM shit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple HTTP assertions against one handler.

3 participants