Skip to content

[testify-expert] Improve Test Quality: pkg/cli/update_cooldown_test.go #29511

@github-actions

Description

@github-actions

Overview

The test file pkg/cli/update_cooldown_test.go was selected for quality improvement by the Daily Testify Uber Super Expert. This file tests the release cooldown functionality and is already well-written with good testify usage. This issue provides specific, actionable refinements to elevate it further.

Current State

  • Test File: pkg/cli/update_cooldown_test.go
  • Source File: pkg/cli/update_cooldown.go
  • Test Functions: 12 test functions
  • Lines of Code: 217 lines
  • Exported Source Functions: parseCoolDownFlag, isExemptFromCoolDown, checkReleaseCoolDown, checkReleaseCoolDownWithDate, formatCoolDownDuration

Test Quality Analysis

Strengths ✅

  1. Build tag present//go:build !integration is correctly placed at the top.
  2. Testify fully adopted — No plain if err != nil { t.Fatal(...) } anti-patterns; require/assert used throughout.
  3. Good assertion messages — Most assertions include format strings with relevant context.
  4. Table-driven tests for pure functionsTestParseCoolDownFlag and TestFormatCoolDownDuration correctly use table-driven tests.
  5. Proper fail-fast vs continue patternrequire.NoError used on the critical path, assert.* for non-fatal checks.
🎯 Areas for Improvement

1. Table-Driven Consolidation for checkReleaseCoolDown variants

TestCheckReleaseCoolDown_OldRelease, TestCheckReleaseCoolDown_RecentRelease, TestCheckReleaseCoolDown_OldReleaseReturnsPublishedAt, and TestCheckReleaseCoolDown_FetchError are four separate test functions with nearly identical setup (stub getReleasePublishedAtFn, defer restore, call the function). They would benefit from being a single table-driven test.

Current (separate functions):

func TestCheckReleaseCoolDown_OldRelease(t *testing.T) {
    orig := getReleasePublishedAtFn
    defer func() { getReleasePublishedAtFn = orig }()
    getReleasePublishedAtFn = func(_ context.Context, _, _ string) (time.Time, error) {
        return time.Now().Add(-10 * 24 * time.Hour), nil
    }
    result := checkReleaseCoolDown(context.Background(), "owner/repo", "v1.2.0", 7*24*time.Hour)
    assert.False(t, result.InCoolDown, "release older than cooldown period should not be blocked")
}

func TestCheckReleaseCoolDown_RecentRelease(t *testing.T) {
    // ... same boilerplate again ...
}

Recommended (table-driven):

func TestCheckReleaseCoolDown(t *testing.T) {
    tests := []struct {
        name           string
        publishedAt    func() time.Time
        fetchErr       error
        coolDown       time.Duration
        wantInCoolDown bool
        wantPublished  bool // PublishedAt should be non-zero
        wantMsgContain string
    }{
        {
            name:           "disabled cooldown",
            publishedAt:    func() time.Time { return time.Now() },
            coolDown:       0,
            wantInCoolDown: false,
        },
        {
            name:           "old release not blocked",
            publishedAt:    func() time.Time { return time.Now().Add(-10 * 24 * time.Hour) },
            coolDown:       7 * 24 * time.Hour,
            wantInCoolDown: false,
            wantPublished:  true,
        },
        {
            name:           "recent release blocked",
            publishedAt:    func() time.Time { return time.Now().Add(-2 * 24 * time.Hour) },
            coolDown:       7 * 24 * time.Hour,
            wantInCoolDown: true,
            wantPublished:  true,
            wantMsgContain: "v1.2.0",
        },
        {
            name:           "fetch error allows update (fail-open)",
            fetchErr:       errors.New("network error"),
            coolDown:       7 * 24 * time.Hour,
            wantInCoolDown: false,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            orig := getReleasePublishedAtFn
            defer func() { getReleasePublishedAtFn = orig }()

            getReleasePublishedAtFn = func(_ context.Context, _, _ string) (time.Time, error) {
                if tt.fetchErr != nil {
                    return time.Time{}, tt.fetchErr
                }
                return tt.publishedAt(), nil
            }

            result := checkReleaseCoolDown(context.Background(), "owner/repo", "v1.2.0", tt.coolDown)
            assert.Equal(t, tt.wantInCoolDown, result.InCoolDown, "InCoolDown mismatch for %q", tt.name)
            if tt.wantPublished {
                assert.False(t, result.PublishedAt.IsZero(), "PublishedAt should be non-zero for %q", tt.name)
            }
            if tt.wantMsgContain != "" {
                assert.Contains(t, result.Message, tt.wantMsgContain, "message should contain tag for %q", tt.name)
            }
        })
    }
}

Why this matters: Consolidating to a table-driven test eliminates ~60 lines of boilerplate, makes adding new scenarios trivial, and makes the test intent clearer at a glance.


2. Named Field in TestIsExemptFromCoolDown table

The TestIsExemptFromCoolDown struct only has repo and want fields. Using tt.repo as the subtest name works, but adding a name field makes the intent of each case explicit and helps distinguish between semantically similar cases.

Current:

tests := []struct {
    repo string
    want bool
}{
    {"actions/checkout", true},
    {"owner/repo", false},
    // ...
}
for _, tt := range tests {
    t.Run(tt.repo, func(t *testing.T) {
        got := isExemptFromCoolDown(tt.repo)
        assert.Equal(t, tt.want, got, ...)
    })
}

Recommended:

tests := []struct {
    name string
    repo string
    want bool
}{
    {name: "actions org is exempt", repo: "actions/checkout", want: true},
    {name: "github org is exempt",  repo: "github/gh-aw", want: true},
    {name: "actions sub-path exempt", repo: "actions/cache/restore", want: true},
    {name: "non-exempt org blocked",   repo: "owner/repo", want: false},
    {name: "prefix-not-actions blocked", repo: "notactions/checkout", want: false},
}
for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) { ... })
}

3. Missing message-content assertions for checkReleaseCoolDownWithDate

TestCheckReleaseCoolDownWithDate_InCoolDown only checks that Message contains the tag (v1.2.0), but the source generates a richer message including "cool down", "remaining", and "cooldown period". Adding assertions for these key phrases protects against message regressions.

Recommended additions:

func TestCheckReleaseCoolDownWithDate_InCoolDown(t *testing.T) {
    publishedAt := time.Now().Add(-2 * 24 * time.Hour)
    result := checkReleaseCoolDownWithDate("owner/repo", "v1.2.0", publishedAt, 7*24*time.Hour)
    assert.True(t, result.InCoolDown, "release 2d old should be in cooldown with 7d window")
    assert.Contains(t, result.Message, "v1.2.0",    "message should mention the tag")
    assert.Contains(t, result.Message, "cool down", "message should mention cooldown")
    assert.Contains(t, result.Message, "remaining", "message should mention remaining time")
    assert.Contains(t, result.Message, "owner/repo","message should mention the repository")
}

4. Missing negative-duration edge case for formatCoolDownDuration

The source code explicitly handles d < 0 by clamping to 0, but the test table doesn't include a negative-duration case. This leaves the clamping logic untested.

Recommended addition to TestFormatCoolDownDuration table:

{-1 * time.Hour, "< 1h"},  // negative duration clamped to 0

5. Missing test for checkReleaseCoolDownWithDatePublishedAt field

The function signature returns a coolDownCheckResult, but when InCoolDown is false the test never verifies whether PublishedAt is zero (it should be, since checkReleaseCoolDownWithDate never sets it — only checkReleaseCoolDown does). Adding this assertion makes the contract explicit.

Recommended:

func TestCheckReleaseCoolDownWithDate_NotInCoolDown(t *testing.T) {
    publishedAt := time.Now().Add(-10 * 24 * time.Hour)
    result := checkReleaseCoolDownWithDate("owner/repo", "v1.2.0", publishedAt, 7*24*time.Hour)
    assert.False(t, result.InCoolDown, "release 10d old should not be in cooldown with 7d window")
    assert.True(t, result.PublishedAt.IsZero(), "checkReleaseCoolDownWithDate should not populate PublishedAt")
    assert.Empty(t, result.Message, "no message expected when not in cooldown")
}
📋 Implementation Guidelines

Priority Order

  1. High: Consolidate TestCheckReleaseCoolDown_* functions into a single table-driven test (eliminates boilerplate, improves extensibility)
  2. Medium: Add name field to TestIsExemptFromCoolDown table
  3. Medium: Add richer message assertions to TestCheckReleaseCoolDownWithDate_InCoolDown
  4. Low: Add negative-duration test case to TestFormatCoolDownDuration
  5. Low: Add PublishedAt zero-value assertion to TestCheckReleaseCoolDownWithDate_NotInCoolDown

Best Practices from scratchpad/testing.md

  • ✅ Use require.* for critical setup (stops test on failure)
  • ✅ Use assert.* for test validations (continues checking)
  • ✅ Write table-driven tests with t.Run() and descriptive names
  • ✅ No mocks or test suites — test real component interactions
  • ✅ Always include helpful assertion messages

Testing Commands

# Run tests for this file
go test -v ./pkg/cli/ -run "TestCheckReleaseCoolDown|TestIsExemptFromCoolDown|TestParseCoolDownFlag|TestFormatCoolDownDuration"

# Run all unit tests
make test-unit

Acceptance Criteria

  • TestCheckReleaseCoolDown_* functions consolidated into a single table-driven TestCheckReleaseCoolDown
  • TestIsExemptFromCoolDown struct includes a name field; subtests use t.Run(tt.name, ...)
  • TestCheckReleaseCoolDownWithDate_InCoolDown asserts on "cool down", "remaining", and "owner/repo" in the message
  • TestFormatCoolDownDuration table includes a negative-duration case (-1 * time.Hour → "< 1h")
  • TestCheckReleaseCoolDownWithDate_NotInCoolDown asserts PublishedAt.IsZero() and Message == ""
  • All tests pass: make test-unit
  • Code follows patterns in scratchpad/testing.md

Additional Context

  • Repository Testing Guidelines: See scratchpad/testing.md for comprehensive testing patterns
  • Example Tests: See pkg/cli/update_cooldown_test.go itself — it is a good baseline already
  • Testify Documentation: https://github.com/stretchr/testify

Priority: Low
Effort: Small (~30-40 lines net reduction after refactoring)
Expected Impact: Cleaner tests, better regression protection for message content, easier addition of new cooldown scenarios

Files Involved:

  • Test file: pkg/cli/update_cooldown_test.go
  • Source file: pkg/cli/update_cooldown.go

Generated by Daily Testify Uber Super Expert · ● 1M ·

  • expires on May 3, 2026, 10:06 AM UTC

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions