Skip to content

Commit 19b61f5

Browse files
authored
Merge pull request #1 from go-pkgz/add-test-utilities
Add file and HTTP test utilities
2 parents 0d3b6ea + e43e7a9 commit 19b61f5

7 files changed

+441
-22
lines changed

CLAUDE.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Go-pkgz/testutils Commands and Guidelines
2+
3+
## Build/Test/Lint Commands
4+
- Build: `go build -race`
5+
- Test all packages: `go test -race -covermode=atomic -coverprofile=profile.cov ./...`
6+
- Run single test: `go test -run=TestName ./path/to/package`
7+
- Run test in verbose mode: `go test -v -run=TestName ./path/to/package`
8+
- Lint: `golangci-lint run`
9+
10+
## Code Style Guidelines
11+
- Use camelCase for functions and variables, with clear descriptive names
12+
- Test files use `_test.go` suffix with table-driven tests using anonymous structs
13+
- Pass context as first parameter when relevant
14+
- Use t.Helper() in test utility functions for better error reporting
15+
- Employ t.Run() for subtests with descriptive names
16+
- Always defer resource cleanup in tests and production code
17+
- Return errors rather than panicking; handle errors explicitly
18+
- Include t.Skip() for integration tests when testing.Short() is true
19+
- Prefer testify's require/assert package for test assertions
20+
- Use proper Go modules import ordering (standard lib, external, internal)
21+
- Always close resources using defer pattern after creation (eg. defer container.Close(ctx))

README.md

+121-10
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,132 @@ Package `testutils` provides useful test helpers.
44

55
## Details
66

7-
- `CaptureStdout`, `CaptureSterr` and `CaptureStdoutAndStderr`: capture stdout, stderr or both for testing purposes. All capture functions are not thread-safe if used in parallel tests, and usually it is better to pass a custom io.Writer to the function under test instead.
7+
### Capture Utilities
88

9-
- `containers`: provides test containers for integration testing:
10-
- `SSHTestContainer`: SSH server container for testing SSH connections and operations
11-
- `PostgresTestContainer`: PostgreSQL database container with automatic database creation
12-
- `MySQLTestContainer`: MySQL database container with automatic database creation
13-
- `MongoTestContainer`: MongoDB container with support for multiple versions (5, 6, 7)
14-
- `LocalstackTestContainer`: LocalStack container with S3 service for AWS testing
9+
- `CaptureStdout`: Captures stdout output from the provided function
10+
- `CaptureStderr`: Captures stderr output from the provided function
11+
- `CaptureStdoutAndStderr`: Captures both stdout and stderr from the provided function
12+
13+
These capture utilities are useful for testing functions that write directly to stdout/stderr. They redirect the standard outputs to a buffer and return the captured content as a string.
14+
15+
**Important Note**: The capture functions are not thread-safe if used in parallel tests. For concurrent tests, it's better to pass a custom io.Writer to the function under test instead.
16+
17+
### File Utilities
18+
19+
- `WriteTestFile`: Creates a temporary file with specified content and returns its path. The file is automatically cleaned up after the test completes.
20+
21+
### HTTP Test Utilities
22+
23+
- `MockHTTPServer`: Creates a test HTTP server with the given handler. Returns the server URL and a cleanup function.
24+
- `HTTPRequestCaptor`: Returns a request captor and an HTTP handler that captures and records HTTP requests for later inspection.
25+
26+
### Test Containers
27+
28+
The `containers` package provides several test containers for integration testing:
29+
30+
- `SSHTestContainer`: SSH server container for testing SSH connections and operations
31+
- `PostgresTestContainer`: PostgreSQL database container with automatic database creation
32+
- `MySQLTestContainer`: MySQL database container with automatic database creation
33+
- `MongoTestContainer`: MongoDB container with support for multiple versions (5, 6, 7)
34+
- `LocalstackTestContainer`: LocalStack container with S3 service for AWS testing
1535

1636
## Install and update
1737

1838
`go get -u github.com/go-pkgz/testutils`
1939

20-
### Example usage:
40+
## Example Usage
41+
42+
### Capture Functions
43+
44+
```go
45+
// Capture stdout
46+
func TestMyFunction(t *testing.T) {
47+
output := testutils.CaptureStdout(t, func() {
48+
fmt.Println("Hello, World!")
49+
})
50+
51+
assert.Equal(t, "Hello, World!\n", output)
52+
}
53+
54+
// Capture stderr
55+
func TestErrorOutput(t *testing.T) {
56+
errOutput := testutils.CaptureStderr(t, func() {
57+
fmt.Fprintln(os.Stderr, "Error message")
58+
})
59+
60+
assert.Equal(t, "Error message\n", errOutput)
61+
}
62+
63+
// Capture both stdout and stderr
64+
func TestBothOutputs(t *testing.T) {
65+
stdout, stderr := testutils.CaptureStdoutAndStderr(t, func() {
66+
fmt.Println("Standard output")
67+
fmt.Fprintln(os.Stderr, "Error output")
68+
})
69+
70+
assert.Equal(t, "Standard output\n", stdout)
71+
assert.Equal(t, "Error output\n", stderr)
72+
}
73+
```
74+
75+
### File Utilities
76+
77+
```go
78+
// Create a temporary test file
79+
func TestWithTempFile(t *testing.T) {
80+
content := "test file content"
81+
filePath := testutils.WriteTestFile(t, content)
82+
83+
// Use the file in your test
84+
data, err := os.ReadFile(filePath)
85+
require.NoError(t, err)
86+
assert.Equal(t, content, string(data))
87+
88+
// No need to clean up - it happens automatically when the test ends
89+
}
90+
```
91+
92+
### HTTP Test Utilities
93+
94+
```go
95+
// Create a mock HTTP server
96+
func TestWithMockServer(t *testing.T) {
97+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
98+
w.WriteHeader(http.StatusOK)
99+
w.Write([]byte("response"))
100+
})
101+
102+
serverURL, _ := testutils.MockHTTPServer(t, handler)
103+
104+
// Make requests to the server
105+
resp, err := http.Get(serverURL + "/path")
106+
require.NoError(t, err)
107+
defer resp.Body.Close()
108+
109+
assert.Equal(t, http.StatusOK, resp.StatusCode)
110+
}
111+
112+
// Capture and inspect HTTP requests
113+
func TestWithRequestCaptor(t *testing.T) {
114+
// Create a request captor
115+
captor, handler := testutils.HTTPRequestCaptor(t, nil)
116+
117+
// Create a server with the capturing handler
118+
serverURL, _ := testutils.MockHTTPServer(t, handler)
119+
120+
// Make a request
121+
http.Post(serverURL+"/api", "application/json",
122+
strings.NewReader(`{"key":"value"}`))
123+
124+
// Inspect the captured request
125+
req, _ := captor.GetRequest(0)
126+
assert.Equal(t, http.MethodPost, req.Method)
127+
assert.Equal(t, "/api", req.Path)
128+
assert.Equal(t, `{"key":"value"}`, string(req.Body))
129+
}
130+
```
131+
132+
### Test Containers
21133

22134
```go
23135
// PostgreSQL test container
@@ -84,5 +196,4 @@ func TestWithS3(t *testing.T) {
84196
})
85197
require.NoError(t, err)
86198
}
87-
```
88-
199+
```

capture_test.go

+6-12
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"os"
66
"strings"
77
"testing"
8+
9+
"github.com/stretchr/testify/require"
810
)
911

1012
func TestCaptureStdout(t *testing.T) {
@@ -38,9 +40,7 @@ func TestCaptureStdout(t *testing.T) {
3840
for _, tt := range tests {
3941
t.Run(tt.name, func(t *testing.T) {
4042
got := CaptureStdout(t, tt.f)
41-
if got != tt.want {
42-
t.Errorf("CaptureStdout() = %q, want %q", got, tt.want)
43-
}
43+
require.Equal(t, tt.want, got, "CaptureStdout() returned unexpected output")
4444
})
4545
}
4646
}
@@ -76,9 +76,7 @@ func TestCaptureStderr(t *testing.T) {
7676
for _, tt := range tests {
7777
t.Run(tt.name, func(t *testing.T) {
7878
got := CaptureStderr(t, tt.f)
79-
if got != tt.want {
80-
t.Errorf("CaptureStderr() = %q, want %q", got, tt.want)
81-
}
79+
require.Equal(t, tt.want, got, "CaptureStderr() returned unexpected output")
8280
})
8381
}
8482
}
@@ -129,12 +127,8 @@ func TestCaptureStdoutAndStderr(t *testing.T) {
129127
for _, tt := range tests {
130128
t.Run(tt.name, func(t *testing.T) {
131129
gotOut, gotErr := CaptureStdoutAndStderr(t, tt.f)
132-
if gotOut != tt.wantOut {
133-
t.Errorf("CaptureStdoutAndStderr() stdout = %q, want %q", gotOut, tt.wantOut)
134-
}
135-
if gotErr != tt.wantErr {
136-
t.Errorf("CaptureStdoutAndStderr() stderr = %q, want %q", gotErr, tt.wantErr)
137-
}
130+
require.Equal(t, tt.wantOut, gotOut, "CaptureStdoutAndStderr() stdout returned unexpected output")
131+
require.Equal(t, tt.wantErr, gotErr, "CaptureStdoutAndStderr() stderr returned unexpected output")
138132
})
139133
}
140134
}

file_utils.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package testutils
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
// WriteTestFile creates a temporary file with the given content and returns its path.
10+
// The file is automatically cleaned up after the test completes.
11+
func WriteTestFile(t *testing.T, content string) string {
12+
t.Helper()
13+
14+
// create a temporary directory for the file
15+
tempDir, err := os.MkdirTemp("", "testutils-")
16+
if err != nil {
17+
t.Fatalf("failed to create temp directory: %v", err)
18+
}
19+
20+
// register cleanup to remove the temporary directory and its contents
21+
t.Cleanup(func() {
22+
if err := os.RemoveAll(tempDir); err != nil {
23+
t.Logf("failed to remove temporary directory %s: %v", tempDir, err)
24+
}
25+
})
26+
27+
// create a file with a unique name
28+
tempFile := filepath.Join(tempDir, "testfile")
29+
if err := os.WriteFile(tempFile, []byte(content), 0o600); err != nil {
30+
t.Fatalf("failed to write test file: %v", err)
31+
}
32+
33+
return tempFile
34+
}

file_utils_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package testutils
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestWriteTestFile(t *testing.T) {
11+
// test creating a file with content
12+
content := "test content"
13+
filePath := WriteTestFile(t, content)
14+
15+
// check if file exists
16+
_, err := os.Stat(filePath)
17+
require.False(t, os.IsNotExist(err), "WriteTestFile did not create file at %s", filePath)
18+
19+
// check content
20+
data, err := os.ReadFile(filePath)
21+
require.NoError(t, err, "Failed to read test file")
22+
require.Equal(t, content, string(data), "File content doesn't match expected")
23+
24+
// file should be cleaned up automatically at the end of the test
25+
}

0 commit comments

Comments
 (0)