| name | go-testing-cli |
| description | Testing patterns for Go CLI tools - command tests, golden tests, table-driven tests, filesystem tests, benchmarks, and fuzz tests. Use when writing or reviewing tests for Harvx. Use when this capability is needed. |
| metadata | {"author":"abdelazizmoustafa10m"} |
Go CLI Testing Patterns
1 -- Command Tests
Execute CLI commands with arguments, capture stdout/stderr/exit code, and verify behavior.
Rules
- No network calls unless explicitly mocked.
- Test flag validation and error messages.
- Test
--help output exists and is well-formatted.
- Use
bytes.Buffer for capturing output.
- Test both success and error paths.
package cli_test
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/harvx/harvx/internal/cli"
)
func TestGenerateCommand_Success(t *testing.T) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
cmd := cli.NewRootCmd()
cmd.SetOut(stdout)
cmd.SetErr(stderr)
cmd.SetArgs([]string{"generate", "testdata/sample-repo"})
err := cmd.Execute()
require.NoError(t, err)
assert.Contains(t, stdout.String(), "files discovered")
assert.Empty(t, stderr.String())
}
func TestGenerateCommand_InvalidFlag(t *testing.T) {
stderr := new(bytes.Buffer)
cmd := cli.NewRootCmd()
cmd.SetErr(stderr)
cmd.SetArgs([]string{"generate", "--format", "invalid"})
err := cmd.Execute()
require.Error(t, err)
assert.Contains(t, err.Error(), "unknown format")
}
func TestGenerateCommand_HelpOutput(t *testing.T) {
stdout := new(bytes.Buffer)
cmd := cli.NewRootCmd()
cmd.SetOut(stdout)
cmd.SetArgs([]string{"generate", "--help"})
err := cmd.Execute()
require.NoError(t, err)
output := stdout.String()
assert.Contains(t, output, "Usage:")
assert.Contains(t, output, "Examples:")
assert.Contains(t, output, "--output-file")
assert.Contains(t, output, "--profile")
}
2 -- Table-Driven Tests
Rules
- Use for multiple input/output combinations.
- Name test cases so failures are immediately diagnosable.
- Split success and error paths into separate test functions.
- Use
testify/require for fatal assertions, testify/assert for soft checks.
- Mark independent subtests with
t.Parallel().
func TestTokenizer_Count_Success(t *testing.T) {
tok, err := tokenizer.New()
require.NoError(t, err)
tests := []struct {
name string
content string
want int
}{
{name: "empty string", content: "", want: 0},
{name: "single token", content: "hello", want: 1},
{name: "go function", content: "func main() {\n\tfmt.Println(\"hi\")\n}", want: 12},
{name: "unicode", content: "cafe\u0301", want: 3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := tok.Count(tt.content)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestTokenizer_Count_Errors(t *testing.T) {
tests := []struct {
name string
content string
wantErr string
}{
{name: "nil tokenizer panics recovered", content: "x", wantErr: "tokenizer not initialized"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var tok *tokenizer.Tokenizer
_, err := tok.Count(tt.content)
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
})
}
}
3 -- Golden Tests
Use golden tests for JSON schema verification and stable text output.
Rules
- Provide
-update flag to regenerate golden files.
- Normalize timestamps, hashes, and absolute paths before comparison.
- Ensure deterministic ordering in output.
- Add golden tests when output format changes (catches regressions).
- Store golden files in
testdata/expected-output/.
package output_test
import (
"flag"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/harvx/harvx/internal/output"
)
var update = flag.Bool("update", false, "update golden files")
func TestMarkdownRenderer_Golden(t *testing.T) {
tests := []struct {
name string
input output.RenderInput
golden string
}{
{
name: "default profile",
input: loadTestInput(t, "default"),
golden: "testdata/expected-output/default.md",
},
{
name: "minimal profile",
input: loadTestInput(t, "minimal"),
golden: "testdata/expected-output/minimal.md",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := output.RenderMarkdown(tt.input)
require.NoError(t, err)
normalized := normalize(string(actual))
if *update {
err := os.WriteFile(tt.golden, []byte(normalized), 0644)
require.NoError(t, err, "failed to update golden file")
return
}
expected, err := os.ReadFile(tt.golden)
require.NoError(t, err, "golden file missing; run with -update to create")
assert.Equal(t, normalize((expected)), normalized)
})
}
}
{
s = timestampRe.ReplaceAllString(s, )
s = strings.ReplaceAll(s, , )
lines := strings.Split(s, )
i, line := lines {
lines[i] = strings.TrimRight(line, )
}
strings.Join(lines, )
}
4 -- Filesystem Tests
Rules
- Use
t.TempDir() for isolated directories (auto-cleaned).
- Use
t.Helper() in all setup helper functions.
- Create realistic test structures that mirror actual repo layouts.
- Test
.gitignore behavior with real ignore files.
func setupTestRepo(t *testing.T) string {
t.Helper()
dir := t.TempDir()
createFile(t, dir, "main.go", "package main\n\nfunc main() {}\n")
createFile(t, dir, "lib/util.go", "package lib\n\nfunc Helper() {}\n")
createFile(t, dir, "README.md", "# Test Project\n")
createFile(t, dir, ".gitignore", "*.log\n/dist/\nnode_modules/\n")
createFile(t, dir, "dist/bundle.js", "compiled code")
createFile(t, dir, "debug.log", "log content")
createFile(t, dir, "icon.png", string([]byte{0x89, 0x50, 0x4E, 0x47}))
return dir
}
func createFile(t *testing.T, base, rel, content string) {
t.Helper()
path := filepath.Join(base, rel)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0755))
require.NoError(t, os.WriteFile(path, []byte(content), 0644))
}
func createDir(t *testing.T, base, rel string) {
t.Helper()
require.NoError(t, os.MkdirAll(filepath.Join(base, rel), 0755))
}
func TestWalker_IgnoresGitignorePatterns(t *testing.T) {
dir := setupTestRepo(t)
w, err := discovery.New(discovery.Options{Root: dir})
require.NoError(t, err)
files, err := w.Walk(context.Background())
require.NoError(t, err)
paths := extractPaths(files)
assert.Contains(t, paths, )
assert.Contains(t, paths, )
assert.Contains(t, paths, )
assert.NotContains(t, paths, )
assert.NotContains(t, paths, )
}
[] {
paths := ([], (files))
i, f := files {
paths[i] = f.RelPath
}
paths
}
5 -- Benchmark Tests
Add benchmarks for performance-critical paths: file walking, token counting, output rendering.
func BenchmarkWalker_LargeRepo(b *testing.B) {
dir := setupLargeBenchRepo(b)
w, err := discovery.New(discovery.Options{Root: dir})
require.NoError(b, err)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := w.Walk(context.Background())
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkTokenizer_Count(b *testing.B) {
tok, _ := tokenizer.New()
content := strings.Repeat("func main() { fmt.Println(\"hello\") }\n", 1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
tok.Count(content)
}
}
func setupLargeBenchRepo(b *testing.B) string {
b.Helper()
dir := b.TempDir()
for i := 0; i < 1000; i++ {
path := filepath.Join(dir, fmt.Sprintf("pkg%d/file.go", i))
os.MkdirAll(filepath.Dir(path), 0755)
os.WriteFile(path, []byte(fmt.Sprintf("package pkg%d\n", i)), 0644)
}
return dir
}
6 -- Fuzz Tests
Add fuzz tests for security-critical code: secret redaction, config parsing, input validation.
func FuzzRedactor_Redact(f *testing.F) {
f.Add("normal text without secrets")
f.Add("aws_secret_access_key = AKIAIOSFODNN7EXAMPLE")
f.Add("password: hunter2\ntoken: ghp_abc123")
f.Add(strings.Repeat("A", 10000))
f.Add("")
redactor := security.NewRedactor(security.DefaultPatterns())
f.Fuzz(func(t *testing.T, input string) {
output := redactor.Redact(input)
if len(output) > len(input)*2+100 {
t.Errorf("output unexpectedly large: input=%d output=%d", len(input), len(output))
}
})
}
7 -- Test Execution Cheatsheet
go test ./...
go test ./internal/cli/...
go test -v -run TestGenerate ./...
go test -race ./...
go test -count=1 ./...
go test -run TestGolden -update ./...
go test -bench=. ./...
go test -bench=BenchmarkWalker -benchmem
go test -fuzz=FuzzRedactor ./internal/security/
Converted and distributed by TomeVault — claim your Tome and manage your conversions.