Skip to content

Commit 11cbb3e

Browse files
authored
Merge pull request #258 from githubnext/copilot/review-go-module-testify
2 parents e6988b8 + 90e5ea1 commit 11cbb3e

4 files changed

Lines changed: 108 additions & 49 deletions

File tree

.golangci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ linters:
1010
- misspell
1111
- unconvert
1212
# testifylint disabled due to requiring extensive test refactoring
13+
# Re-enable after addressing existing issues: golangci-lint run --enable=testifylint
1314
# gosec disabled in golangci-lint due to configuration complexity
1415
# Can be enabled separately with: gosec -exclude=G101,G104,G115,G301,G302,G304,G306 ./...
1516
disable:
@@ -20,6 +21,8 @@ linters:
2021
- gosec
2122

2223
linters-settings:
24+
testifylint:
25+
enable-all: true
2326
# Configuration for disabled linters is kept for reference if they are re-enabled
2427
errcheck:
2528
exclude-functions:

AGENTS.md

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,87 @@ args = ["run", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "-i", "ghcr.io/gith
8787
- Godoc comments for exports
8888
- Mock external dependencies (Docker, network)
8989

90+
## Testing with Testify
91+
92+
**ALWAYS use testify for test assertions** - The project uses [stretchr/testify](https://github.com/stretchr/testify) for all test assertions.
93+
94+
### Assert vs Require
95+
96+
- **`require`**: Use for critical checks - test stops on failure
97+
- **`assert`**: Use for non-critical checks - test continues on failure
98+
99+
```go
100+
import (
101+
"github.com/stretchr/testify/assert"
102+
"github.com/stretchr/testify/require"
103+
)
104+
105+
func TestExample(t *testing.T) {
106+
result, err := DoSomething()
107+
require.NoError(t, err) // Stop if error - can't continue
108+
assert.Equal(t, "expected", result.Field) // Continue even if fails
109+
}
110+
```
111+
112+
### Bound Asserters
113+
114+
For tests with multiple assertions, use bound asserters to reduce repetition:
115+
116+
```go
117+
func TestMultipleAssertions(t *testing.T) {
118+
assert := assert.New(t)
119+
require := require.New(t)
120+
121+
result := GetResult()
122+
require.NotNil(result) // Stop if nil
123+
124+
// Cleaner - no need to pass 't' repeatedly
125+
assert.Equal("value1", result.Field1)
126+
assert.Equal("value2", result.Field2)
127+
assert.True(result.Active)
128+
}
129+
```
130+
131+
### Specific Assertion Methods
132+
133+
Use specific assertion methods instead of generic ones for better error messages:
134+
135+
```go
136+
// ❌ Avoid generic assertions
137+
assert.True(t, len(items) == 0)
138+
assert.True(t, err == nil)
139+
assert.True(t, strings.Contains(msg, "error"))
140+
141+
// ✅ Use specific assertions
142+
assert.Empty(t, items)
143+
assert.NoError(t, err)
144+
assert.Contains(t, msg, "error")
145+
```
146+
147+
### Common Patterns
148+
149+
```go
150+
// Length checking
151+
assert.Len(t, items, 5, "Expected 5 items")
152+
153+
// Unordered slice comparison
154+
assert.ElementsMatch(t, expected, actual, "Slices should contain same elements")
155+
156+
// Nil checking
157+
assert.NotNil(t, obj, "Object should not be nil")
158+
assert.Nil(t, err, "Error should be nil")
159+
160+
// Error checking (prefer NoError over Nil for errors)
161+
assert.NoError(t, err, "Operation should succeed")
162+
assert.Error(t, err, "Operation should fail")
163+
164+
// HTTP status codes
165+
assert.Equal(t, http.StatusOK, response.StatusCode, "Should return 200 OK")
166+
167+
// JSON comparison (ignores formatting)
168+
assert.JSONEq(t, expectedJSON, actualJSON)
169+
```
170+
90171
## Linting
91172

92173
**golangci-lint** is integrated and runs as part of `make lint`:
@@ -96,9 +177,17 @@ args = ["run", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "-i", "ghcr.io/gith
96177
- Install: `make install` (installs golangci-lint v2.8.0)
97178
- Run manually: `golangci-lint run --timeout=5m`
98179

99-
**Note**: Some linters (gosec, testifylint) are disabled to minimize noise. Enable them for stricter checks:
180+
**testifylint**: Available but disabled due to requiring extensive test refactoring across the codebase.
181+
- Automatically catches common testify mistakes:
182+
- Suggests `assert.Empty(t, x)` instead of `assert.True(t, len(x) == 0)`
183+
- Suggests `assert.True(t, x)` instead of `assert.Equal(t, true, x)`
184+
- Suggests `assert.NoError(t, err)` instead of `assert.Nil(t, err)`
185+
- To run on specific files: `golangci-lint run --enable=testifylint --timeout=5m <files>`
186+
- To run on entire codebase: `golangci-lint run --enable=testifylint --timeout=5m`
187+
188+
**Note**: Some linters (gosec, testifylint, errcheck) are disabled to minimize noise. Enable them for stricter checks:
100189
```bash
101-
golangci-lint run --enable=gosec,testifylint --timeout=5m
190+
golangci-lint run --enable=gosec,testifylint,errcheck --timeout=5m
102191
```
103192

104193
## Test Structure

internal/server/routed_test.go

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,28 +43,19 @@ func TestCloseEndpoint_Success(t *testing.T) {
4343
httpServer.Handler.ServeHTTP(w, req)
4444

4545
// Verify response
46-
if w.Code != http.StatusOK {
47-
t.Errorf("Expected status 200, got %d", w.Code)
48-
}
46+
assert.Equal(t, http.StatusOK, w.Code, "Close endpoint should return 200 OK")
4947

5048
var response map[string]interface{}
51-
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
52-
t.Fatalf("Failed to decode response: %v", err)
53-
}
49+
require.NoError(t, json.NewDecoder(w.Body).Decode(&response), "Failed to decode response")
5450

5551
// Check response fields
56-
if status, ok := response["status"].(string); !ok || status != "closed" {
57-
t.Errorf("Expected status 'closed', got %v", response["status"])
58-
}
59-
60-
if msg, ok := response["message"].(string); !ok || msg != "Gateway shutdown initiated" {
61-
t.Errorf("Expected message 'Gateway shutdown initiated', got %v", response["message"])
62-
}
52+
assert.Equal(t, "closed", response["status"], "Expected status 'closed'")
53+
assert.Equal(t, "Gateway shutdown initiated", response["message"], "Expected shutdown message")
6354

6455
// Should report 2 servers terminated
65-
if count, ok := response["serversTerminated"].(float64); !ok || count != 2 {
66-
t.Errorf("Expected serversTerminated 2, got %v", response["serversTerminated"])
67-
}
56+
serversTerminated, ok := response["serversTerminated"].(float64)
57+
require.True(t, ok, "serversTerminated should be a number")
58+
assert.InDelta(t, 2.0, serversTerminated, 0.01, "Expected 2 servers terminated")
6859

6960
// Verify server is marked as shutdown
7061
assert.True(t, us.IsShutdown(), "Expected server to be marked as shutdown")

internal/server/unified_test.go

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,9 @@ func TestUnifiedServer_GetServerIDs(t *testing.T) {
2424
defer us.Close()
2525

2626
serverIDs := us.GetServerIDs()
27-
if len(serverIDs) != 2 {
28-
t.Errorf("Expected 2 server IDs, got %d", len(serverIDs))
29-
}
27+
assert.Len(t, serverIDs, 2, "Expected 2 server IDs")
3028

31-
expectedIDs := map[string]bool{"github": true, "fetch": true}
32-
for _, id := range serverIDs {
33-
if !expectedIDs[id] {
34-
t.Errorf("Unexpected server ID: %s", id)
35-
}
36-
}
29+
assert.ElementsMatch(t, []string{"github", "fetch"}, serverIDs, "Server IDs should match expected values")
3730
}
3831

3932
func TestUnifiedServer_SessionManagement(t *testing.T) {
@@ -59,17 +52,9 @@ func TestUnifiedServer_SessionManagement(t *testing.T) {
5952
session, exists := us.sessions[sessionID]
6053
us.sessionMu.RUnlock()
6154

62-
if !exists {
63-
t.Error("Session not found after creation")
64-
}
65-
66-
if session.Token != token {
67-
t.Errorf("Expected token '%s', got '%s'", token, session.Token)
68-
}
69-
70-
if session.SessionID != sessionID {
71-
t.Errorf("Expected session ID '%s', got '%s'", sessionID, session.SessionID)
72-
}
55+
assert.True(t, exists, "Session should exist after creation")
56+
assert.Equal(t, token, session.Token, "Session token should match")
57+
assert.Equal(t, sessionID, session.SessionID, "Session ID should match")
7358
}
7459

7560
func TestUnifiedServer_GetSessionKeys(t *testing.T) {
@@ -91,18 +76,9 @@ func TestUnifiedServer_GetSessionKeys(t *testing.T) {
9176
}
9277

9378
keys := us.getSessionKeys()
94-
assert.Equal(t, len(sessions), len(keys))
79+
assert.Len(t, keys, len(sessions), "Number of session keys should match")
9580

96-
keyMap := make(map[string]bool)
97-
for _, key := range keys {
98-
keyMap[key] = true
99-
}
100-
101-
for _, expected := range sessions {
102-
if !keyMap[expected] {
103-
t.Errorf("Session key '%s' not found", expected)
104-
}
105-
}
81+
assert.ElementsMatch(t, sessions, keys, "Session keys should match expected sessions")
10682
}
10783

10884
func TestUnifiedServer_GetToolsForBackend(t *testing.T) {

0 commit comments

Comments
 (0)