Testing Comprehensive testing strategies and best practices for your CLI application.
Test Organization Test Structure Tests are co-located with the code they test:
cmd/
├── root.go
├── root_test.go # Tests for root command
├── hello.go
└── hello_test.go # Tests for hello command
Test Naming Follow Go testing conventions:
Files : *_test.go Functions : TestFunctionName Benchmarks : BenchmarkFunctionName Examples : ExampleFunctionName Running Tests Basic Test Commands 1
2
3
4
5
6
7
8
9
10
11
12 # Run all tests
make test
go test ./...
# Run tests with verbose output
go test -v ./...
# Run specific package tests
go test ./cmd
# Run specific test function
go test -run TestHelloCommand ./cmd
Test Coverage 1
2
3
4
5
6
7
8
9
10
11
12 # Generate coverage report
make test-coverage
# View coverage in terminal
go test -cover ./...
# Generate detailed coverage
go test -coverprofile= coverage.out ./...
go tool cover -html= coverage.out -o coverage.html
# View coverage by function
go tool cover -func= coverage.out
Writing Tests Command Testing Pattern The testing approach separates business logic from command handling for better testability:
1. Action Function Testing Test the business logic directly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 func TestHelloAction ( t * testing . T ) {
tests := [] struct {
name string
input string
expected string
wantErr bool
}{
{
name : "default name" ,
input : "" ,
expected : "Hello, World!\n" ,
wantErr : false ,
},
{
name : "custom name" ,
input : "Alice" ,
expected : "Hello, Alice!\n" ,
wantErr : false ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
buf := & bytes . Buffer {}
err := HelloAction ( tt . input , buf )
if ( err != nil ) != tt . wantErr {
t . Errorf ( "HelloAction() error = %v, wantErr %v" , err , tt . wantErr )
}
if got := buf . String (); got != tt . expected {
t . Errorf ( "HelloAction() = %q, want %q" , got , tt . expected )
}
})
}
}
2. Command Integration Testing Test the full command execution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 func TestHelloCommand ( t * testing . T ) {
tests := [] struct {
name string
args [] string
expected string
}{
{
name : "default greeting" ,
args : [] string { "hello" },
expected : "Hello, World!\n" ,
},
{
name : "custom name" ,
args : [] string { "hello" , "--name" , "Alice" },
expected : "Hello, Alice!\n" ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
buf := new ( bytes . Buffer )
rootCmd := & cobra . Command { Use : "test" }
rootCmd . AddCommand ( helloCmd )
rootCmd . SetOut ( buf )
rootCmd . SetArgs ( tt . args )
err := rootCmd . Execute ()
if err != nil {
t . Errorf ( "Execute() error = %v" , err )
}
if got := buf . String (); got != tt . expected {
t . Errorf ( "Expected output %q, got %q" , tt . expected , got )
}
})
}
}
Error Testing 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 func TestCommandErrors ( t * testing . T ) {
tests := [] struct {
name string
args [] string
expectError bool
errorMsg string
}{
{
name : "invalid flag" ,
args : [] string { "--invalid-flag" },
expectError : true ,
errorMsg : "unknown flag" ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
cmd := yourCommand () // Create command instance
cmd . SetArgs ( tt . args )
err := cmd . Execute ()
if tt . expectError {
if err == nil {
t . Error ( "Expected error but got none" )
}
if ! strings . Contains ( err . Error (), tt . errorMsg ) {
t . Errorf ( "Expected error containing %q, got %q" , tt . errorMsg , err . Error ())
}
} else {
if err != nil {
t . Errorf ( "Unexpected error: %v" , err )
}
}
})
}
}
Test Types Unit Tests Test individual functions and methods:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 func TestFormatGreeting ( t * testing . T ) {
tests := [] struct {
name string
input string
expected string
}{
{ "empty name" , "" , "Hello, World!" },
{ "with name" , "Alice" , "Hello, Alice!" },
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
result := formatGreeting ( tt . input )
if result != tt . expected {
t . Errorf ( "formatGreeting(%q) = %q, want %q" , tt . input , result , tt . expected )
}
})
}
}
Integration Tests Test command execution end-to-end:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 func TestFullCommandExecution ( t * testing . T ) {
// Create temporary directory for test artifacts
tmpDir := t . TempDir ()
// Execute command with file output
cmd := exec . Command ( "./cli-template" , "hello" , "--output" , tmpDir + "/greeting.txt" )
err := cmd . Run ()
if err != nil {
t . Fatalf ( "Command failed: %v" , err )
}
// Verify output file
content , err := os . ReadFile ( tmpDir + "/greeting.txt" )
if err != nil {
t . Fatalf ( "Failed to read output file: %v" , err )
}
expected := "Hello, World!\n"
if string ( content ) != expected {
t . Errorf ( "Expected file content %q, got %q" , expected , string ( content ))
}
}
Benchmark Tests Measure performance:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 func BenchmarkHelloCommand ( b * testing . B ) {
cmd := & cobra . Command {
Use : "hello" ,
Run : func ( cmd * cobra . Command , args [] string ) {
cmd . Print ( "Hello, World!" )
},
}
for i := 0 ; i < b . N ; i ++ {
buf := new ( bytes . Buffer )
cmd . SetOut ( buf )
cmd . SetArgs ([] string {})
cmd . Execute ()
}
}
Test Utilities Test Helpers Create reusable test utilities:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 // testutil/helpers.go
package testutil
import (
"bytes"
"testing"
"github.com/spf13/cobra"
)
func ExecuteCommand ( t * testing . T , cmd * cobra . Command , args [] string ) ( string , error ) {
t . Helper ()
buf := new ( bytes . Buffer )
cmd . SetOut ( buf )
cmd . SetErr ( buf )
cmd . SetArgs ( args )
err := cmd . Execute ()
return buf . String (), err
}
func AssertOutput ( t * testing . T , got , want string ) {
t . Helper ()
if got != want {
t . Errorf ( "Output mismatch:\nGot: %q\nWant: %q" , got , want )
}
}
Mock Dependencies Use interfaces for testable code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 // Interface for external dependencies
type FileWriter interface {
WriteFile ( filename string , data [] byte , perm os . FileMode ) error
}
// Production implementation
type OSFileWriter struct {}
func ( w OSFileWriter ) WriteFile ( filename string , data [] byte , perm os . FileMode ) error {
return os . WriteFile ( filename , data , perm )
}
// Test mock
type MockFileWriter struct {
WrittenFiles map [ string ][] byte
}
func ( w * MockFileWriter ) WriteFile ( filename string , data [] byte , perm os . FileMode ) error {
if w . WrittenFiles == nil {
w . WrittenFiles = make ( map [ string ][] byte )
}
w . WrittenFiles [ filename ] = data
return nil
}
Continuous Integration GitHub Actions The CI workflow runs tests automatically:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 test :
runs-on : ubuntu-latest
strategy :
matrix :
go-version : [ 1.21.x , 1.22.x , 1.23.x ]
steps :
- uses : actions/checkout@v4
- name : Set up Go
uses : actions/setup-go@v5
with :
go-version : ${{ matrix.go-version }}
- name : Run tests
run : go test -v -race -coverprofile=coverage.out ./...
Test Configuration Environment variables for tests:
# Enable race detection
export GORACE = "halt_on_error=1"
# Test timeout
export GOTESTSUM_TIMEOUT = 300s
# Coverage threshold
export COVERAGE_THRESHOLD = 80
Best Practices Test Organization Table-driven tests : Use for multiple similar scenarios Subtests : Group related test cases with t.Run() Helper functions : Extract common test setup Test fixtures : Use consistent test data Test Reliability Deterministic : Tests should produce same results Independent : Tests shouldn't depend on each other Fast : Keep tests quick to encourage frequent running Clear failures : Provide helpful error messages Coverage Goals Aim for 80%+ : Good coverage without obsessing over 100% Test edge cases : Error conditions, empty inputs, boundaries Focus on critical paths : Prioritize important functionality Ignore generated code : Exclude auto-generated files Common Patterns 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 // Setup and teardown
func TestWithSetup ( t * testing . T ) {
// Setup
oldEnv := os . Getenv ( "TEST_VAR" )
os . Setenv ( "TEST_VAR" , "test-value" )
defer os . Setenv ( "TEST_VAR" , oldEnv ) // Cleanup
// Test implementation
}
// Parallel tests
func TestParallel ( t * testing . T ) {
tests := [] struct { ... }{}
for _ , tt := range tests {
tt := tt // Capture range variable
t . Run ( tt . name , func ( t * testing . T ) {
t . Parallel () // Run in parallel
// Test implementation
})
}
}