Skip to content

Commit 66bd8c2

Browse files
chmouelclaude
authored andcommitted
tests: enforce conventions for go Test
Enforce our convention on test to use table driven tests and better naming across the repository. Add s imple check in linters.yaml to enforce this. Update AGENTS.md so agents can respect it. Co-Authored-By: Claude <[email protected]> Signed-off-by: Chmouel Boudjnah <[email protected]>
1 parent 1ce8d6f commit 66bd8c2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+246
-184
lines changed

.tekton/linter.yaml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,60 @@ spec:
169169
image: docker.io/golang:1.26
170170
workingDir: $(workspaces.source.path)
171171
script: |
172+
git config --global --add safe.directory $(workspaces.source.path)
172173
make lint-e2e-naming
173174
175+
- name: check-test-conventions
176+
displayName: "Check Go test naming and assertion conventions"
177+
image: docker.io/golang:1.26
178+
workingDir: $(workspaces.source.path)
179+
script: |
180+
#!/usr/bin/env bash
181+
set -eu
182+
git config --global --add safe.directory $(workspaces.source.path)
183+
184+
if [[ "{{ headers['X-Github-Event'] }}" != "pull_request" ]]; then
185+
echo "Not a pull request, skipping test convention check"
186+
exit 0
187+
fi
188+
189+
base_sha="{{ body.pull_request.base.sha }}"
190+
if [[ -z "$base_sha" ]]; then
191+
echo "Base SHA is empty, skipping"
192+
exit 0
193+
fi
194+
195+
files=$(git diff --name-only "${base_sha}..HEAD" -- '*.go' | grep -v '^vendor/' | grep '_test\.go$' || true)
196+
if [ -z "$files" ]; then
197+
echo "No test files modified."
198+
exit 0
199+
fi
200+
201+
failed=0
202+
while IFS= read -r file; do
203+
[ -z "$file" ] && continue
204+
[ ! -f "$file" ] && continue
205+
206+
bad_names=$(grep -n '^func Test[A-Za-z]*_[A-Za-z]' "$file" || true)
207+
if [ -n "$bad_names" ]; then
208+
echo "ERROR: $file: test function names must use PascalCase (no underscores):"
209+
echo "$bad_names"
210+
failed=1
211+
fi
212+
213+
bad_asserts=$(grep -En '\bt\.(Errorf|Fatalf|Fatal|Error)\(' "$file" || true)
214+
if [ -n "$bad_asserts" ]; then
215+
echo "WARNING: $file: use gotest.tools/v3/assert instead of t.Errorf/t.Fatalf/t.Fatal/t.Error:"
216+
echo "$bad_asserts"
217+
fi
218+
done <<< "$files"
219+
220+
if [ "$failed" -ne 0 ]; then
221+
echo "Fix violations above. See AGENTS.md for testing conventions."
222+
exit 1
223+
fi
224+
echo "All modified test files pass convention checks."
225+
174226
- name: check-generated-schemas
175227
displayName: "Check generated OpenAPI schemas"
176228
image: docker.io/golang:1.26

AGENTS.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,55 @@
2828

2929
## Testing
3030

31-
- **Unit tests**: Use `gotest.tools/v3` (never testify)
31+
### Test Structure
32+
33+
- **Use table-driven tests**: Anonymous struct pattern with `tests := []struct{...}{...}`
34+
- **No underscores in test names**: Use PascalCase like `TestGetTektonDir`, not `TestGetTektonDir_Something`
35+
- **Subtest naming**: Use descriptive `name` field for `t.Run(tt.name, ...)`
36+
- **Complex setup**: Add `setup func(t *testing.T, ...)` field to test struct instead of creating separate test functions
37+
- **Example**:
38+
39+
```go
40+
func TestGetTektonDir(t *testing.T) {
41+
tests := []struct {
42+
name string
43+
event *info.Event
44+
setup func(t *testing.T, mux *http.ServeMux)
45+
}{
46+
{
47+
name: "simple case",
48+
event: &info.Event{...},
49+
setup: func(t *testing.T, mux *http.ServeMux) {
50+
t.Helper()
51+
mux.HandleFunc("/repos/...", ...)
52+
},
53+
},
54+
}
55+
for _, tt := range tests {
56+
t.Run(tt.name, func(t *testing.T) { ... })
57+
}
58+
}
59+
```
60+
61+
### Assertions
62+
63+
- **Use `gotest.tools/v3/assert`** (never testify or pkg/assert)
64+
- `assert.NilError(t, err)` - verify no error
65+
- `assert.Assert(t, condition, msg)` - general assertions
66+
- `assert.Equal(t, expected, actual)` - equality checks
67+
- `assert.ErrorContains(t, err, substring)` - error validation
68+
69+
### Test Types
70+
71+
- **Unit tests**: Fast, focused, use mocks from `pkg/test/github`
3272
- **E2E tests**: Require specific setup - always ask user to run and copy output
3373
- **Gitea tests**: Most comprehensive and self-contained
34-
- **Commands**: `make test-no-cache`, `make html-coverage`
74+
75+
### Commands
76+
77+
- `make test` - Run test suite
78+
- `make test-no-cache` - Run without cache
79+
- `make html-coverage` - Generate coverage report
3580

3681
## Dependencies
3782

pkg/acl/owners_test.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"testing"
55

66
"golang.org/x/exp/slices"
7+
"gotest.tools/v3/assert"
78
)
89

910
func TestUserInOwnerFile(t *testing.T) {
@@ -127,13 +128,12 @@ func TestUserInOwnerFile(t *testing.T) {
127128
for _, tt := range tests {
128129
t.Run(tt.name, func(t *testing.T) {
129130
got, err := UserInOwnerFile(tt.args.ownersContent, tt.args.ownersAliasesContent, tt.args.sender)
130-
if (err != nil) != tt.wantErr {
131-
t.Errorf("UserInOwnerFile() error = %v, wantErr %v", err, tt.wantErr)
131+
if tt.wantErr {
132+
assert.Assert(t, err != nil)
132133
return
133134
}
134-
if got != tt.want {
135-
t.Errorf("UserInOwnerFile() = %v, want %v", got, tt.want)
136-
}
135+
assert.NilError(t, err)
136+
assert.Equal(t, got, tt.want)
137137
})
138138
}
139139
}
@@ -195,13 +195,9 @@ func TestExpandAliases(t *testing.T) {
195195
// Can't use reflect.DeepEqual to compare the slices as expandAliases
196196
// uses a map for dedup which does not preserve the order.
197197
// We do not care about the order, just the content of the slice.
198-
if len(got) != len(tt.want) {
199-
t.Errorf("expandAliases() got = %v, want %v", got, tt.want)
200-
}
198+
assert.Equal(t, len(got), len(tt.want))
201199
for _, v := range got {
202-
if !slices.Contains(tt.want, v) {
203-
t.Errorf("expandAliases() got = %v, want %v", got, tt.want)
204-
}
200+
assert.Assert(t, slices.Contains(tt.want, v), "got = %v, want %v", got, tt.want)
205201
}
206202
})
207203
}

pkg/acl/regexp_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,8 @@ func TestMatchRegexp(t *testing.T) {
100100
}
101101
for _, tt := range tests {
102102
t.Run(tt.name, func(t *testing.T) {
103-
if got := MatchRegexp(tt.args.reg, tt.args.comment); got != tt.want {
104-
t.Errorf("MatchRegexp() = %v, want %v", got, tt.want)
105-
}
103+
got := MatchRegexp(tt.args.reg, tt.args.comment)
104+
assert.Equal(t, got, tt.want)
106105
})
107106
}
108107
}

pkg/adapter/adapter_test.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,23 +190,17 @@ func TestHandleEvent(t *testing.T) {
190190
defer ts.Close()
191191

192192
req, err := http.NewRequestWithContext(context.Background(), tn.requestType, ts.URL, bytes.NewReader(tn.event))
193-
if err != nil {
194-
t.Fatalf("error creating request: %s", err)
195-
}
193+
assert.NilError(t, err)
196194
req.Header.Set("X-Github-Event", tn.eventType)
197195

198196
resp, err := http.DefaultClient.Do(req)
199-
if err != nil {
200-
t.Fatalf("error sending request: %s", err)
201-
}
197+
assert.NilError(t, err)
202198
defer resp.Body.Close()
203199

204200
if tn.wantLogSnippet != "" {
205201
assert.Assert(t, logCatcher.FilterMessageSnippet(tn.wantLogSnippet).Len() > 0, logCatcher.All())
206202
}
207-
if resp.StatusCode != tn.statusCode {
208-
t.Fatalf("expected status code : %v but got %v ", tn.statusCode, resp.StatusCode)
209-
}
203+
assert.Equal(t, resp.StatusCode, tn.statusCode)
210204
})
211205
}
212206
}

pkg/adapter/incoming_test.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Q1QWaigUQdpFfNCrqwJBANLgWaJV722PhQXOCmR+INvZ7ksIhJVcq/x1l2BYOLw2
5050
QsncVExbMiPa9Oclo5qLuTosS8qwHm1MJEytp3/SkB8=
5151
-----END RSA PRIVATE KEY-----`
5252

53-
func Test_compareSecret(t *testing.T) {
53+
func TestCompareSecret(t *testing.T) {
5454
type args struct {
5555
incomingSecret string
5656
secretValue string
@@ -79,14 +79,12 @@ func Test_compareSecret(t *testing.T) {
7979
}
8080
for _, tt := range tests {
8181
t.Run(tt.name, func(t *testing.T) {
82-
if got := compareSecret(tt.args.incomingSecret, tt.args.secretValue); got != tt.want {
83-
t.Errorf("compareSecret() = %v, want %v", got, tt.want)
84-
}
82+
assert.Equal(t, compareSecret(tt.args.incomingSecret, tt.args.secretValue), tt.want)
8583
})
8684
}
8785
}
8886

89-
func Test_listener_detectIncoming(t *testing.T) {
87+
func TestListenerDetectIncoming(t *testing.T) {
9088
const goodURL = "https://matched/by/incoming"
9189
envRemove := env.PatchAll(t, map[string]string{"SYSTEM_NAMESPACE": "pipelinesascode"})
9290
defer envRemove()
@@ -855,7 +853,7 @@ func Test_listener_detectIncoming(t *testing.T) {
855853
}
856854
}
857855

858-
func Test_listener_processIncoming(t *testing.T) {
856+
func TestListenerProcessIncoming(t *testing.T) {
859857
tests := []struct {
860858
name string
861859
want provider.Interface
@@ -1081,7 +1079,7 @@ func TestApplyIncomingParams(t *testing.T) {
10811079
}
10821080
}
10831081

1084-
func Test_detectIncoming_legacy_warning(t *testing.T) {
1082+
func TestDetectIncomingLegacyWarning(t *testing.T) {
10851083
ctx, _ := rtesting.SetupFakeContext(t)
10861084
testNamespace := &corev1.Namespace{
10871085
ObjectMeta: metav1.ObjectMeta{
@@ -1180,7 +1178,7 @@ func Test_detectIncoming_legacy_warning(t *testing.T) {
11801178
}
11811179
}
11821180

1183-
func Test_detectIncoming_body_params_are_parsed(t *testing.T) {
1181+
func TestDetectIncomingBodyParamsAreParsed(t *testing.T) {
11841182
ctx, _ := rtesting.SetupFakeContext(t)
11851183
testNamespace := &corev1.Namespace{
11861184
ObjectMeta: metav1.ObjectMeta{
@@ -1237,7 +1235,7 @@ func Test_detectIncoming_body_params_are_parsed(t *testing.T) {
12371235
assert.Assert(t, got)
12381236
}
12391237

1240-
func Test_parseIncomingPayload(t *testing.T) {
1238+
func TestParseIncomingPayload(t *testing.T) {
12411239
tests := []struct {
12421240
name string
12431241
method string

pkg/adapter/sinker_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func setupTestData(t *testing.T, repos []*v1alpha1.Repository) *params.Run {
8585
}
8686

8787
// TestSetupClient_GitHubAppVsOther tests the different code paths for GitHub Apps vs other providers.
88-
func TestSetupClient_GitHubAppVsOther(t *testing.T) {
88+
func TestSetupClientGitHubAppVsOther(t *testing.T) {
8989
t.Parallel()
9090

9191
tests := []struct {
@@ -407,10 +407,10 @@ func TestFindMatchingRepository(t *testing.T) {
407407
}
408408
}
409409

410-
// TestProcessEvent_SkipCI_PushEvent tests the skip-CI logic for push events.
410+
// TestProcessEventSkipCIPushEvent tests the skip-CI logic for push events.
411411
// This tests that the SkipCI check works correctly for push events where the
412412
// commit message is available directly in the webhook payload (event.SHATitle).
413-
func TestProcessEvent_SkipCI_PushEvent(t *testing.T) {
413+
func TestProcessEventSkipCIPushEvent(t *testing.T) {
414414
t.Parallel()
415415

416416
tests := []struct {
@@ -473,10 +473,10 @@ func TestProcessEvent_SkipCI_PushEvent(t *testing.T) {
473473
}
474474
}
475475

476-
// TestGetCommitInfo_SetsHasSkipCommand tests that GetCommitInfo correctly sets
476+
// TestGetCommitInfoSetsHasSkipCommand tests that GetCommitInfo correctly sets
477477
// HasSkipCommand when the commit message contains skip-CI commands.
478478
// This tests the full flow that processEvent uses for PR events.
479-
func TestGetCommitInfo_SetsHasSkipCommand(t *testing.T) {
479+
func TestGetCommitInfoSetsHasSkipCommand(t *testing.T) {
480480
t.Parallel()
481481

482482
tests := []struct {
@@ -554,10 +554,10 @@ func TestGetCommitInfo_SetsHasSkipCommand(t *testing.T) {
554554
}
555555
}
556556

557-
// TestProcessEvent_SkipCI_Integration documents the skip-CI flow integration.
557+
// TestProcessEventSkipCIIntegration documents the skip-CI flow integration.
558558
// This is a documentation test that verifies the skip-CI decision logic
559559
// matches what's implemented in sinker.go processEvent().
560-
func TestProcessEvent_SkipCI_Integration(t *testing.T) {
560+
func TestProcessEventSkipCIIntegration(t *testing.T) {
561561
t.Parallel()
562562

563563
tests := []struct {

pkg/cli/iostreams_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,23 @@ func TestNewIOStreams(t *testing.T) {
1515
assert.Assert(t, ios.ErrOut != nil)
1616
}
1717

18-
func TestIOStreams_ColorEnabled(t *testing.T) {
18+
func TestIOStreamsColorEnabled(t *testing.T) {
1919
ios := &IOStreams{colorEnabled: true}
2020
assert.Equal(t, ios.ColorEnabled(), true)
2121

2222
ios.SetColorEnabled(false)
2323
assert.Equal(t, ios.ColorEnabled(), false)
2424
}
2525

26-
func TestIOStreams_ColorSupport256(t *testing.T) {
26+
func TestIOStreamsColorSupport256(t *testing.T) {
2727
ios := &IOStreams{is256enabled: true}
2828
assert.Equal(t, ios.ColorSupport256(), true)
2929

3030
ios.is256enabled = false
3131
assert.Equal(t, ios.ColorSupport256(), false)
3232
}
3333

34-
func TestIOStreams_IsStdoutTTY(t *testing.T) {
34+
func TestIOStreamsIsStdoutTTY(t *testing.T) {
3535
tests := []struct {
3636
name string
3737
setup func() *IOStreams

pkg/cmd/tknpac/bootstrap/github_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"gotest.tools/v3/assert"
88
)
99

10-
func Test_generateManifest(t *testing.T) {
10+
func TestGenerateManifest(t *testing.T) {
1111
type args struct {
1212
opts *bootstrapOpts
1313
}
@@ -39,7 +39,7 @@ func Test_generateManifest(t *testing.T) {
3939
}
4040
}
4141

42-
func Test_getGHClient(t *testing.T) {
42+
func TestGetGHClient(t *testing.T) {
4343
tests := []struct {
4444
name string
4545
URL string

pkg/cmd/tknpac/webhook/add_test.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"io"
66
"testing"
77

8-
"github.com/google/go-cmp/cmp"
98
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
109
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
1110
"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
@@ -14,6 +13,7 @@ import (
1413
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/clients"
1514
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
1615
testclient "github.com/openshift-pipelines/pipelines-as-code/pkg/test/clients"
16+
"gotest.tools/v3/assert"
1717
corev1 "k8s.io/api/core/v1"
1818
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1919
rtesting "knative.dev/pkg/reconciler/testing"
@@ -230,13 +230,12 @@ func TestWebhookAdd(t *testing.T) {
230230
Info: info.Info{Kube: &info.KubeOpts{Namespace: tt.opts.Namespace}},
231231
}
232232
io, out := newIOStream()
233-
if err := add(ctx, tt.opts, cs, io,
234-
tt.repoName, tt.pacNamespace); (err != nil) != tt.wantErr {
235-
t.Errorf("add() error = %v, wantErr %v", err, tt.wantErr)
233+
err := add(ctx, tt.opts, cs, io, tt.repoName, tt.pacNamespace)
234+
if tt.wantErr {
235+
assert.Assert(t, err != nil)
236236
} else {
237-
if res := cmp.Diff(out.String(), tt.wantMsg); res != "" {
238-
t.Errorf("Diff %s:", res)
239-
}
237+
assert.NilError(t, err)
238+
assert.Equal(t, tt.wantMsg, out.String())
240239
}
241240
})
242241
}

0 commit comments

Comments
 (0)