Skip to content

Commit 795945f

Browse files
committed
add file and HTTP test utilities
- Add WriteTestFile function for creating temporary test files - Add MockHTTPServer for easy HTTP server testing - Add HTTPRequestCaptor for inspecting HTTP requests in tests - Enhance README with new utilities and examples 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 0d3b6ea commit 795945f

6 files changed

+489
-10
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+
```

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

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package testutils
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
func TestWriteTestFile(t *testing.T) {
9+
// test creating a file with content
10+
content := "test content"
11+
filePath := WriteTestFile(t, content)
12+
13+
// check if file exists
14+
if _, err := os.Stat(filePath); os.IsNotExist(err) {
15+
t.Errorf("WriteTestFile did not create file at %s", filePath)
16+
}
17+
18+
// check content
19+
data, err := os.ReadFile(filePath)
20+
if err != nil {
21+
t.Errorf("Failed to read test file: %v", err)
22+
}
23+
24+
if string(data) != content {
25+
t.Errorf("Expected content %q, got %q", content, string(data))
26+
}
27+
28+
// file should be cleaned up automatically at the end of the test
29+
}

http_utils.go

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package testutils
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"net/http/httptest"
8+
"sync"
9+
"testing"
10+
)
11+
12+
// MockHTTPServer creates a test HTTP server with the given handler.
13+
// Returns the server URL and a function to close it.
14+
func MockHTTPServer(t *testing.T, handler http.Handler) (serverURL string, cleanup func()) {
15+
t.Helper()
16+
17+
server := httptest.NewServer(handler)
18+
19+
cleanup = func() {
20+
server.Close()
21+
}
22+
23+
// register cleanup with t.Cleanup to ensure the server is closed
24+
// even if the test fails
25+
t.Cleanup(cleanup)
26+
27+
return server.URL, cleanup
28+
}
29+
30+
// RequestRecord holds information about a captured HTTP request
31+
type RequestRecord struct {
32+
Method string
33+
Path string
34+
Headers http.Header
35+
Body []byte
36+
}
37+
38+
// RequestCaptor captures HTTP requests for inspection in tests
39+
type RequestCaptor struct {
40+
mu sync.Mutex
41+
requests []RequestRecord
42+
}
43+
44+
// Len returns the number of captured requests
45+
func (c *RequestCaptor) Len() int {
46+
c.mu.Lock()
47+
defer c.mu.Unlock()
48+
return len(c.requests)
49+
}
50+
51+
// GetRequest returns the request at the specified index
52+
func (c *RequestCaptor) GetRequest(idx int) (RequestRecord, bool) {
53+
c.mu.Lock()
54+
defer c.mu.Unlock()
55+
56+
if idx < 0 || idx >= len(c.requests) {
57+
return RequestRecord{}, false
58+
}
59+
60+
return c.requests[idx], true
61+
}
62+
63+
// GetRequests returns all captured requests
64+
func (c *RequestCaptor) GetRequests() []RequestRecord {
65+
c.mu.Lock()
66+
defer c.mu.Unlock()
67+
68+
// return a copy to avoid race conditions
69+
result := make([]RequestRecord, len(c.requests))
70+
copy(result, c.requests)
71+
return result
72+
}
73+
74+
// Reset clears all captured requests
75+
func (c *RequestCaptor) Reset() {
76+
c.mu.Lock()
77+
defer c.mu.Unlock()
78+
c.requests = nil
79+
}
80+
81+
// add records a new request
82+
func (c *RequestCaptor) add(rec RequestRecord) {
83+
c.mu.Lock()
84+
defer c.mu.Unlock()
85+
c.requests = append(c.requests, rec)
86+
}
87+
88+
// HTTPRequestCaptor returns a request captor and HTTP handler that captures requests
89+
// The returned handler will forward requests to the provided next handler if not nil
90+
func HTTPRequestCaptor(t *testing.T, next http.Handler) (*RequestCaptor, http.Handler) {
91+
t.Helper()
92+
93+
captor := &RequestCaptor{
94+
requests: []RequestRecord{},
95+
}
96+
97+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
98+
// create a record from the request
99+
record := RequestRecord{
100+
Method: r.Method,
101+
Path: r.URL.Path,
102+
Headers: r.Header.Clone(),
103+
}
104+
105+
// read and store the body if present
106+
if r.Body != nil {
107+
// read body
108+
bodyBytes, err := io.ReadAll(r.Body)
109+
if err != nil {
110+
t.Logf("failed to read request body: %v", err)
111+
}
112+
113+
// store the body in the record
114+
record.Body = bodyBytes
115+
116+
// replace the body for downstream handlers
117+
r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
118+
}
119+
120+
// add the record to the captor
121+
captor.add(record)
122+
123+
// forward the request if a next handler is provided
124+
if next != nil {
125+
next.ServeHTTP(w, r)
126+
}
127+
})
128+
129+
return captor, handler
130+
}

0 commit comments

Comments
 (0)