Files
golib/TESTING.md
2025-12-21 17:46:31 +01:00

899 lines
20 KiB
Markdown

# Testing Documentation
Comprehensive testing guide for the `github.com/nabbar/golib` library and all its subpackages.
---
## Table of Contents
- [Test Suite Statistics](#test-suite-statistics)
- [Quick Start](#quick-start)
- [Test Framework](#test-framework)
- [Running Tests](#running-tests)
- [Basic Testing](#basic-testing)
- [Race Detection](#race-detection)
- [Coverage Analysis](#coverage-analysis)
- [Package-Specific Testing](#package-specific-testing)
- [Coverage Report](#coverage-report)
- [High Coverage Packages](#high-coverage-packages)
- [Packages Needing Improvement](#packages-needing-improvement)
- [Untested Packages](#untested-packages)
- [Writing Tests](#writing-tests)
- [Test Structure](#test-structure)
- [Ginkgo v2 Guidelines](#ginkgo-v2-guidelines)
- [Gomega Matchers](#gomega-matchers)
- [Best Practices](#best-practices)
- [Troubleshooting](#troubleshooting)
- [CI Integration](#ci-integration)
---
## Test Suite Statistics
**Latest Test Run Results** (from `./coverage-report.sh`):
```
Total Packages: 165
Packages with Tests: 131 (79.4%)
Packages without Tests: 34 (20.6%)
Test Specifications: 11,818
Test Assertions: 23,080
Benchmarks: 151
Pending Tests: 18
Skipped Tests: 6
Average Coverage: 75.49%
Packages ≥80%: 71/131 (54.2%)
Packages at 100%: 16/131 (12.2%)
Race Conditions: 0 (verified with CGO_ENABLED=1 go test -race)
Thread Safety: ✅ All concurrent operations validated
```
**Coverage Distribution:**
| Range | Count | Percentage | Examples |
|-------|-------|------------|----------|
| 100% | 16 | 12.2% | errors/pool, logger/gorm, router/authheader, semaphore/sem |
| 90-99% | 27 | 20.6% | atomic, version, size, prometheus/metrics |
| 80-89% | 28 | 21.4% | ioutils, mail/queuer, context, runner |
| 70-79% | 16 | 12.2% | cobra, viper, socket/client/* |
| 60-69% | 9 | 6.9% | config, logger, database/kvmap |
| <60% | 35 | 26.7% | archive, aws, httpserver |
---
## Quick Start
### Running All Tests
```bash
# Standard test run (all packages)
go test ./...
# Verbose output with details
go test -v ./...
# With race detector (recommended)
CGO_ENABLED=1 go test -race ./...
# With coverage
go test -cover ./...
# Complete test suite (as used in CI)
go test -timeout=10m -v -cover -covermode=atomic ./...
```
### Using Coverage Report Script
```bash
# Run comprehensive coverage analysis
./coverage-report.sh
# Output includes:
# - Coverage statistics per package
# - Packages without tests
# - Packages below 80% coverage
# - Recommendations for improvement
```
### Expected Output
```
Total Packages: 165
Packages with Tests: 127
Test Specifications: 10,964
Average Coverage: 73.9%
PACKAGES WITHOUT TESTS
• archive/archive
• aws/bucket
• config/const
• ...
PACKAGES BELOW 80% COVERAGE
• archive 8.60%
• artifact 23.40%
• aws 5.40%
• ...
```
---
## Test Framework
### Ginkgo v2
Behavior-driven development (BDD) testing framework used across all subpackages.
**Key Features:**
- Spec organization with `Describe`, `Context`, `It`
- `BeforeEach` / `AfterEach` for setup/teardown
- `BeforeAll` / `AfterAll` for suite-level setup
- Ordered specs for sequential tests
- Focused specs (`FIt`, `FContext`) for debugging
- `Eventually` / `Consistently` for async assertions
- Table-driven tests with `DescribeTable`
**Installation:**
```bash
go install github.com/onsi/ginkgo/v2/ginkgo@latest
```
**Documentation:** [Ginkgo v2 Docs](https://onsi.github.io/ginkgo/)
### Gomega
Matcher library for expressive assertions.
**Common Matchers:**
- `Expect(x).To(Equal(y))` - equality
- `Expect(err).ToNot(HaveOccurred())` - error checking
- `Expect(x).To(BeNumerically(">=", y))` - numeric comparison
- `Expect(ch).To(BeClosed())` - channel state
- `Eventually(func)` - async assertion
- `Consistently(func)` - sustained assertion
**Documentation:** [Gomega Docs](https://onsi.github.io/gomega/)
### gmeasure
Performance measurement for Ginkgo tests (used in several packages).
**Usage Example:**
```go
experiment := gmeasure.NewExperiment("Operation Name")
AddReportEntry(experiment.Name, experiment)
experiment.Sample(func(idx int) {
experiment.MeasureDuration("metric_name", func() {
// Code to measure
})
}, gmeasure.SamplingConfig{N: 100, Duration: 5 * time.Second})
stats := experiment.GetStats("metric_name")
```
**Packages Using gmeasure:**
- ioutils/aggregator (performance benchmarks)
- ioutils/multi (write operation metrics)
- monitor/* (system metrics)
**Documentation:** [gmeasure Package](https://pkg.go.dev/github.com/onsi/gomega/gmeasure)
---
## Running Tests
### Basic Testing
```bash
# Run all tests in all packages
go test ./...
# Verbose output (recommended for CI)
go test -v ./...
# Run specific package
go test ./logger
# Run with timeout (important for long-running tests)
go test -timeout 5m ./...
# Skip long-running tests
go test -short ./...
# Run tests matching pattern
go test -run TestLogger ./logger
# With Ginkgo focus
go test -ginkgo.focus="should handle concurrent writes" ./ioutils/aggregator
```
### Race Detection
**Critical for concurrency testing:**
```bash
# Enable race detector (all packages)
CGO_ENABLED=1 go test -race ./...
# Verbose with race detection
CGO_ENABLED=1 go test -race -v ./...
# Full suite with race detection (CI command)
CGO_ENABLED=1 go test -race -timeout=10m -v -cover -covermode=atomic ./...
# Specific package with race detector
CGO_ENABLED=1 go test -race ./ioutils/aggregator
```
**Note:** Race detector adds ~10x overhead. Some tests may take longer.
**Results:** Zero data races detected across all 10,735 specs.
### Coverage Analysis
```bash
# Coverage percentage for all packages
go test -cover ./...
# Coverage profile
go test -coverprofile=coverage.out ./...
# HTML coverage report
go tool cover -html=coverage.out
# Coverage by function
go tool cover -func=coverage.out
# Atomic coverage mode (for race detector)
go test -covermode=atomic -coverprofile=coverage.out ./...
# Per-package coverage
go test -cover ./logger
go test -cover ./ioutils/aggregator
go test -cover ./mail/queuer
```
### Package-Specific Testing
**High-Coverage Packages:**
```bash
# ioutils (87.7% average, 772 specs)
go test -v -cover ./ioutils/...
# mail (89.0% average, 970 specs)
go test -v -cover ./mail/...
# errors (87.6%, 305 specs)
go test -v -cover ./errors/...
# version (93.8%, 173 specs)
go test -v -cover ./version/...
```
**Packages Needing More Tests:**
```bash
# archive (8.6%, 89 specs) - needs improvement
go test -v -cover ./archive/...
# aws (5.4%, 220 specs) - needs improvement
go test -v -cover ./aws/...
# httpserver (52.5%, 84 specs) - moderate coverage
go test -v -cover ./httpserver/...
```
---
## Coverage Report
### Coverage Report Script
The repository includes `coverage-report.sh`, a comprehensive script that analyzes test coverage across all packages.
**Usage:**
```bash
# Run full coverage analysis
./coverage-report.sh
# Output is also saved to a file (optional)
./coverage-report.sh > coverage-full.txt
```
**What it provides:**
- **Overall Statistics**: Total packages, tested packages, average coverage
- **Per-Package Metrics**: Coverage %, specs count, assertions, benchmarks
- **Issue Detection**: Packages without tests, packages below 80% coverage
- **Detailed Breakdown**: Test execution time, pending tests, skipped tests
**Output Example:**
```
Total Packages: 165
Packages with Tests: 127 (77.0%)
Test Specifications: 10,964
Average Coverage: 73.9%
PACKAGES WITHOUT TESTS
• archive/archive
• aws/bucket
...
PACKAGES BELOW 80% COVERAGE
• archive 8.60%
• aws 5.40%
...
```
This script is used to generate all coverage statistics shown in this document and the main README.
---
### High Coverage Packages (≥90%)
**Packages at 100% Coverage:**
| Package | Specs | Assertions | Notes |
|---------|-------|------------|-------|
| errors/pool | 83 | 122 | Thread-safe error pooling |
| httpserver/types | 32 | 53 | Type definitions |
| ioutils/bufferReadCloser | 57 | 138 | Buffered reader with closer |
| ioutils/delim | 198 | 329 | Delimiter-based stream processing |
| ioutils/iowrapper | 114 | 179 | Generic I/O wrappers |
| ioutils/nopwritecloser | 54 | 140 | No-op writer closer |
| logger/gorm | 34 | 76 | GORM logger integration |
| logger/hookstderr | 30 | 64 | Stderr output hook |
| logger/hookstdout | 30 | 64 | Stdout output hook |
| monitor/info | 95 | 262 | System information collection |
| prometheus/types | 36 | 112 | Prometheus type definitions |
| router/authheader | 11 | 29 | Authorization header parsing |
| semaphore/sem | 66 | 117 | Semaphore implementation |
| semaphore | 33 | 55 | Semaphore base |
**Packages 90-99% Coverage:**
- **artifact/client**: 98.6% (21 specs) - Artifact client interface
- **mail/smtp/tlsmode**: 98.8% (165 specs) - SMTP TLS mode handling
- **monitor/status**: 98.4% (181 specs) - Status reporting
- **network/protocol**: 98.7% (298 specs) - Network protocol helpers
- **cache/item**: 96.7% (21 specs) - Cache item implementation
- **logger/hashicorp**: 96.6% (89 specs) - Hashicorp logger adapter
- **router/auth**: 96.3% (12 specs) - Authentication middleware
- **semaphore/bar**: 96.6% (68 specs) - Semaphore with progress bar
- **prometheus/metrics**: 95.5% (179 specs) - Custom metrics
- **status/control**: 95.0% (102 specs) - Status control
- **size**: 95.4% (352 specs) - Byte size arithmetic
- **prometheus/bloom**: 94.7% (45 specs) - Bloom filter metrics
- **version**: 93.8% (173 specs) - Semantic versioning
- **mail/smtp/config**: 92.7% (222 specs) - SMTP configuration
- **atomic**: 91.8% (49 specs) - Generic atomic types
- **duration**: 91.5% (179 specs) - Duration extensions
- **encoding/aes**: 91.5% (126 specs) - AES encryption
- **router**: 91.0% (61 specs) - Gin-based router
- **duration/big**: 91.0% (250 specs) - Big integer duration
- **mail/queuer**: 90.8% (102 specs) - Email queuing
- **mail/smtp**: 90.1% (104 specs) - SMTP client
- **logger/hookwriter**: 90.2% (31 specs) - Generic writer hook
- **runner/ticker**: 90.2% (88 specs) - Ticker management
### Packages Needing Improvement (<40%)
**Critical Priority (0-20% coverage):**
| Package | Coverage | Specs | Status |
|---------|----------|-------|--------|
| artifact/s3aws | 2.0% | 1 | Needs tests |
| aws | 5.4% | 220 | Partial tests |
| artifact/jfrog | 6.1% | 2 | Needs tests |
| ftpclient | 6.2% | 22 | Needs tests |
| archive | 8.6% | 89 | Needs extensive tests |
| artifact/github | 8.6% | 1 | Needs tests |
| artifact/gitlab | 13.5% | 2 | Needs tests |
| database/gorm | 19.6% | 41 | Needs improvement |
| logger/hookfile | 19.6% | 22 | Needs improvement |
**Medium Priority (20-40% coverage):**
- artifact (23.4%)
- database/kvdriver (38.4%)
- config/components/aws (40.7%)
- config/components/database (39.0%)
### Untested Packages
**38 packages without test files:**
Infrastructure packages (primarily type definitions and utilities):
- archive/archive, archive/archive/tar, archive/archive/types, archive/archive/zip
- archive/compress, archive/helper
- aws/bucket, aws/configAws, aws/configCustom, aws/group
- aws/helper, aws/http, aws/multipart, aws/object
- aws/policy, aws/pusher, aws/role, aws/user
- config/const, config/types
- database/kvtypes
- encoding (base package)
- httpserver/testhelpers
- ioutils/maxstdio
- ldap, monitor/types, nats, oauth
- pidcontroller, pprof, prometheus/webmetrics
- request, runner (base package)
- semaphore/types
- socket (base package), socket/client, socket/config, socket/server
**Note:** Many untested packages are interface definitions, constants, or types packages that may not require separate tests if covered by parent package tests.
---
## Writing Tests
### Test Structure
**File Organization:**
Each package follows this structure:
```
package/
├── package_suite_test.go - Suite setup and global helpers
├── feature_test.go - Feature-specific tests
├── concurrency_test.go - Concurrency tests (if applicable)
├── errors_test.go - Error handling tests
├── benchmark_test.go - Performance benchmarks
└── example_test.go - Runnable examples
```
**Test Template:**
```go
var _ = Describe("ComponentName", func() {
var (
component ComponentType
ctx context.Context
cancel context.CancelFunc
)
BeforeEach(func() {
ctx, cancel = context.WithCancel(context.Background())
component = New(...)
})
AfterEach(func() {
if component != nil {
component.Close()
}
cancel()
time.Sleep(10 * time.Millisecond) // Cleanup grace period
})
Context("when testing feature X", func() {
It("should behave correctly", func() {
// Test code
Expect(result).To(Equal(expected))
})
})
})
```
### Ginkgo v2 Guidelines
**Spec Organization:**
```go
Describe("Top-level component", func() {
Context("when condition A", func() {
It("should do X", func() {
// Test
})
It("should do Y", func() {
// Test
})
})
Context("when condition B", func() {
It("should do Z", func() {
// Test
})
})
})
```
**Async Testing:**
```go
// Use Eventually for async operations
Eventually(func() bool {
return component.IsReady()
}, 2*time.Second, 10*time.Millisecond).Should(BeTrue())
// Use Consistently for sustained conditions
Consistently(func() bool {
return component.IsRunning()
}, 1*time.Second, 50*time.Millisecond).Should(BeTrue())
```
### Gomega Matchers
**Common Patterns:**
```go
// Error checking
Expect(err).ToNot(HaveOccurred())
Expect(err).To(MatchError("expected error"))
// Equality
Expect(value).To(Equal(expected))
Expect(value).To(BeNumerically(">=", minimum))
// Collections
Expect(slice).To(ContainElement(item))
Expect(slice).To(HaveLen(5))
Expect(map).To(HaveKey("key"))
// Types
Expect(value).To(BeNil())
Expect(value).To(BeAssignableToTypeOf(Type{}))
// Channels
Expect(ch).To(BeClosed())
Expect(ch).To(Receive(&value))
```
---
## Best Practices
### ✅ DO
**1. Use `Eventually` for Async Operations:**
```go
// ✅ GOOD: Wait for condition
Eventually(func() bool {
return server.IsRunning()
}, 2*time.Second, 10*time.Millisecond).Should(BeTrue())
// ❌ BAD: Fixed sleep
time.Sleep(100 * time.Millisecond)
Expect(server.IsRunning()).To(BeTrue())
```
**2. Protect Shared State:**
```go
// ✅ GOOD: Thread-safe access
var (
mu sync.Mutex
count int
)
writer := func(p []byte) (int, error) {
mu.Lock()
defer mu.Unlock()
count++
return len(p), nil
}
// ❌ BAD: Race condition
var count int
writer := func(p []byte) (int, error) {
count++ // RACE!
return len(p), nil
}
```
**3. Clean Up Resources:**
```go
// ✅ GOOD: Always cleanup
AfterEach(func() {
if component != nil {
component.Close()
}
cancel()
time.Sleep(50 * time.Millisecond)
})
// ❌ BAD: No cleanup
AfterEach(func() {
cancel() // Missing Close()
})
```
**4. Use Descriptive Test Names:**
```go
// ✅ GOOD: Clear intent
It("should return error when connection is closed", func() {
// ...
})
// ❌ BAD: Vague
It("test error", func() {
// ...
})
```
**5. Test Edge Cases:**
```go
// Test nil, empty, boundary values
Context("with nil input", func() {
It("should handle gracefully", func() {
err := component.Process(nil)
Expect(err).To(HaveOccurred())
})
})
Context("with empty string", func() {
It("should return appropriate error", func() {
err := component.Validate("")
Expect(err).To(MatchError("empty input"))
})
})
```
### ❌ DON'T
**1. Don't Ignore Race Detector Warnings:**
```bash
# Always run with race detector during development
CGO_ENABLED=1 go test -race ./...
```
**2. Don't Use Fixed Timeouts:**
```go
// ❌ BAD: Brittle on slow systems
time.Sleep(100 * time.Millisecond)
// ✅ GOOD: Adaptive waiting
Eventually(condition, timeout, interval).Should(BeTrue())
```
**3. Don't Share State Between Tests:**
```go
// ❌ BAD: Global state
var globalCounter int
It("test 1", func() {
globalCounter++ // Affects other tests!
})
// ✅ GOOD: Isolated state
var counter int
BeforeEach(func() {
counter = 0 // Reset for each test
})
```
**4. Don't Skip Error Checking:**
```go
// ❌ BAD: Ignoring errors
result, _ := operation()
// ✅ GOOD: Check all errors
result, err := operation()
Expect(err).ToNot(HaveOccurred())
```
---
## Troubleshooting
### Common Issues
**1. Test Timeout**
```
Error: test timed out after 10m0s
```
**Solution:**
- Increase timeout: `go test -timeout=20m`
- Check for deadlocks in code
- Ensure cleanup completes
- Review Eventually timeouts
**2. Race Condition Detected**
```
WARNING: DATA RACE
```
**Solution:**
- Protect shared variables with mutex
- Use atomic operations
- Review concurrent access patterns
- Add proper synchronization
**3. Flaky Tests**
```
Random failures, not reproducible
```
**Solution:**
- Increase `Eventually` timeouts
- Add proper synchronization
- Run with `-race` to detect issues
- Check resource cleanup
- Avoid fixed `time.Sleep`
**4. Coverage Gaps**
```
coverage: 75.0% (below target 80%)
```
**Solution:**
- Run `go tool cover -html=coverage.out`
- Identify uncovered branches
- Add edge case tests
- Test error paths
- Review package-specific coverage report
**5. Import Cycle**
```
import cycle not allowed
```
**Solution:**
- Refactor packages to break cycle
- Extract common interface
- Use dependency injection
- Move shared types to separate package
### Debug Techniques
**Enable Verbose Output:**
```bash
go test -v ./...
go test -v -ginkgo.v ./package
```
**Focus Specific Test:**
```bash
go test -ginkgo.focus="should handle concurrent writes" ./package
go test -run TestSpecificFunction ./package
```
**Check Goroutine Leaks:**
```go
BeforeEach(func() {
runtime.GC()
initialGoroutines = runtime.NumGoroutine()
})
AfterEach(func() {
runtime.GC()
time.Sleep(100 * time.Millisecond)
leaked := runtime.NumGoroutine() - initialGoroutines
Expect(leaked).To(BeNumerically("<=", 1))
})
```
**Profile Tests:**
```bash
# CPU profiling
go test -cpuprofile=cpu.prof ./package
go tool pprof cpu.prof
# Memory profiling
go test -memprofile=mem.prof ./package
go tool pprof mem.prof
```
---
## CI Integration
### GitHub Actions
```yaml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.24', '1.25', '1.26']
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Test
run: go test -timeout=10m -v -cover -covermode=atomic ./...
- name: Race Detection
run: CGO_ENABLED=1 go test -race -timeout=10m -v ./...
- name: Coverage
run: |
go test -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -html=coverage.out -o coverage.html
- name: Upload Coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.out
```
### GitLab CI
```yaml
test:
image: golang:1.26
stage: test
script:
- go test -timeout=10m -v -cover -covermode=atomic ./...
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
race:
image: golang:1.26
stage: test
script:
- CGO_ENABLED=1 go test -race -timeout=10m -v ./...
coverage:
image: golang:1.26
stage: test
script:
- ./coverage-report.sh
- go tool cover -func=coverage.out
coverage: '/total:\s+\(statements\)\s+(\d+\.\d+)%/'
```
### Pre-commit Hooks
```bash
#!/bin/bash
# .git/hooks/pre-commit
echo "Running golib tests..."
go test -timeout=2m ./...
if [ $? -ne 0 ]; then
echo "Tests failed. Commit aborted."
exit 1
fi
echo "Running race detector..."
CGO_ENABLED=1 go test -race -timeout=5m ./...
if [ $? -ne 0 ]; then
echo "Race conditions detected. Commit aborted."
exit 1
fi
echo "Checking coverage..."
COVERAGE=$(./coverage-report.sh | grep "Average Coverage" | awk '{print $4}' | tr -d '%')
if (( $(echo "$COVERAGE < 70.0" | bc -l) )); then
echo "Coverage $COVERAGE% is below 70%. Commit aborted."
exit 1
fi
echo "All checks passed!"
exit 0
```
---
**Test Suite Maintained By**: [Nicolas JUHEL](https://github.com/nabbar)
**Framework**: Ginkgo v2 / Gomega / gmeasure
**Coverage Target**: ≥80% per package
**Last Updated**: Based on coverage-report.sh analysis