Skip to content

[testify-expert] Improve Test Quality: pkg/agentdrain/anomaly_test.go #43255

Description

@github-actions

Overview

File: pkg/agentdrain/anomaly_test.go
Source pair: pkg/agentdrain/anomaly.go (90 LOC, 3 exported symbols: NewAnomalyDetector, AnomalyDetector.Analyze, buildReason)
Test LOC: 465 | Test functions: 5 | Table cases: ~37


Strengths

  • Excellent boundary testing (rareThreshold=0, size==threshold, size==threshold+1, similarity==threshold, similarity just-below)
  • Correct require/assert split — constructor steps use require, behavior checks use assert
  • Good mutual-exclusivity test (TestAnalyze_FlagMutualExclusivity)
  • Descriptive case names with inline arithmetic comments

Prioritized Improvements

1. Missing / High-Value Tests

Expand missing coverage details

a) Analyze with a nil result argument

Analyze dereferences result.Similarity without a nil guard. Passing nil would panic. A test documenting expected behavior (or a defensive nil-check + test) should exist:

// Missing: nil MatchResult should either return a safe report or be documented as unsupported
{
    name:    "nil result with existing cluster",
    result:  nil, // currently would panic
    isNew:   false,
    cluster: &Cluster{ID: 1, Size: 1},
}

b) AnalyzeEvent — missing empty-event / masking-to-empty path

AnalyzeEvent returns "empty event after masking" error but no test exercises this path:

// Add to anomaly_test.go or miner_test.go:
evtEmpty := AgentEvent{Stage: "plan", Fields: map[string]string{}}
_, _, err := m.AnalyzeEvent(evtEmpty)
// With a mask rule that wipes all tokens, this should return an error.

c) Analyze clamping branch (score > maxScore)

anomaly.go:61 clamps score > maxScore but no test triggers it. Since this is a defensive guard for future weighting changes, a comment explaining it is untestable with current flag semantics is useful.

d) buildReason integration round-trip via Analyze

All 8 buildReason permutations are tested in isolation, but the "new template + low similarity" case is documented as never produced by Analyze. An explicit assertion in TestAnalyze_FlagMutualExclusivity checking report.Reason for each case would close this gap.

2. Testify Assertion Upgrades

Expand assertion improvement details

a) Use assert.Zero for exact-zero score cases

// Before
assert.InDelta(t, 0.0, report.AnomalyScore, 1e-9, "AnomalyScore mismatch")

// After (exact-zero cases only)
assert.Zero(t, report.AnomalyScore, "AnomalyScore should be zero when no anomaly")

b) Use assert.ErrorContains instead of assert.Contains(t, err.Error(), ...)

// Before
assert.Contains(t, err.Error(), tt.wantErr, "error should describe invalid threshold")

// After — avoids manual err.Error() unwrapping, idiomatic in testify v1.7.1+
assert.ErrorContains(t, err, tt.wantErr, "error should describe invalid threshold")

3. Table-Driven Refactor

Expand refactor details

TestAnalyzeEvent uses linear steps (Step 1, Step 2, Step 3) without t.Run. Extracting steps into a sub-table improves readability and extensibility:

// Before: flat linear steps with numbered comments
resultFirst, reportFirst, errFirst := m.AnalyzeEvent(evtPlan)
require.NoError(t, errFirst, "AnalyzeEvent should not fail for first event")
assert.True(t, reportFirst.IsNewTemplate, "IsNewTemplate mismatch for first event")

// After: sub-table with t.Run
steps := []struct {
    name       string
    evt        AgentEvent
    wantNew    bool
    wantScore  float64
    wantReason string
}{
    {"first event trains new template", evtPlan, true, 0.65, "new log template discovered; rare cluster (few observations)"},
    {"second identical event matches cluster", evtPlan, false, 0.15, "rare cluster (few observations)"},
    {"distinct event creates another template", evtFinish, true, 0.65, "new log template discovered; rare cluster (few observations)"},
}
for _, step := range steps {
    t.Run(step.name, func(t *testing.T) {
        result, report, err := m.AnalyzeEvent(step.evt)
        require.NoError(t, err)
        require.NotNil(t, result)
        require.NotNil(t, report)
        assert.Equal(t, step.wantNew, report.IsNewTemplate)
        assert.InDelta(t, step.wantScore, report.AnomalyScore, 1e-9)
        assert.Equal(t, step.wantReason, report.Reason)
    })
}

4. Organization / Readability

Expand readability details
  • The score arithmetic comments (// score = (1.0 + 0.3) / 2.0 = 0.65) are helpful but detached from wantScore. A named constant or code comment co-located with wantScore would survive table reordering better.
  • The TestBuildReason "all flags set" comment accurately notes the combination is unreachable via Analyze. Consider adding a direct assertion to make the invariant machine-checkable.

Acceptance Checklist

  • Add test case for Analyze with a nil result (document expected panic or add nil-guard + test)
  • Add test for AnalyzeEvent returning error on empty-after-masking event
  • Replace assert.Contains(t, err.Error(), ...) with assert.ErrorContains(t, err, ...) in TestNewAnomalyDetector_ThresholdBoundaries
  • Replace assert.InDelta(t, 0.0, ..., 1e-9, ...) with assert.Zero(t, ...) for exact-zero score cases
  • Refactor TestAnalyzeEvent linear steps into a sub-table with t.Run
  • Verify all changes pass: make test-unit

Generated by 🧪 Daily Testify Uber Super Expert · 76.5 AIC · ⌖ 23.8 AIC · ⊞ 5.2K ·

  • expires on Jul 5, 2026, 10:30 AM UTC-08:00

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions