[root] - UPDATE documentation: enhanced README and TESTING guidelines - UPDATE dependencies: bump dependencies [config/components] - UPDATE mail component: apply update following changes in related package - UPDATE smtp component: apply update following changes in related package [mail] - MAJOR REFACTORING - REFACTOR package structure: reorganized into 4 specialized subpackages (queuer, render, sender, smtp) - ADD mail/queuer: mail queue management with counter, monitoring, and comprehensive tests - ADD mail/render: email template rendering with themes and direction handling (moved from mailer package) - ADD mail/sender: email composition and sending with attachments, priorities, and encoding - ADD mail/smtp: SMTP protocol handling with TLS modes and DSN support - ADD documentation: comprehensive README and TESTING for all subpackages - ADD tests: complete test suites with benchmarks, concurrency, and edge cases for all subpackages [mailer] - DEPRECATED - DELETE package: entire package merged into mail/render [mailPooler] - DEPRECATED - DELETE package: entire package merged into mail/queuer [smtp] - DEPRECATED - DELETE root package: entire package moved to mail/smtp - REFACTOR tlsmode: enhanced with encoding, formatting, and viper support (moved to mail/smtp/tlsmode) [size] - ADD documentation: comprehensive README - UPDATE interface: improved Size type methods - UPDATE encoding: enhanced marshaling support - UPDATE formatting: better unit handling and display - UPDATE parsing: improved error handling and validation [socket/server/unix] - ADD platform support: macOS-specific permission handling (perm_darwin.go) - ADD platform support: Linux-specific permission handling (perm_linux.go) - UPDATE listener: improved Unix socket and datagram listeners - UPDATE error handling: enhanced error messages for Unix sockets [socket/server/unixgram] - ADD platform support: macOS-specific permission handling (perm_darwin.go) - ADD platform support: Linux-specific permission handling (perm_linux.go) - UPDATE listener: improved Unix datagram listener - UPDATE error handling: enhanced error messages [socket/server/tcp] - UPDATE listener: improved TCP listener implementation
16 KiB
Testing Guide
Comprehensive testing documentation for the size package, covering test execution, race detection, and quality assurance.
Table of Contents
- Overview
- Quick Start
- Test Framework
- Running Tests
- Test Coverage
- Thread Safety
- Writing Tests
- Best Practices
- Troubleshooting
- CI Integration
Overview
The size package uses Ginkgo v2 (BDD testing framework) and Gomega (matcher library) for comprehensive testing with expressive assertions.
Test Suite Statistics
- Total Specs: 352
- Coverage: 95.4% of statements
- Race Detection: ✅ Zero data races
- Execution Time: ~0.026s (without race), ~1.169s (with race)
Coverage Areas
- Parsing (strings, numbers, complex expressions)
- Arithmetic operations (overflow/underflow handling)
- Type conversions (all numeric types with overflow protection)
- Formatting (various precision levels and unit selection)
- Marshaling (JSON, YAML, TOML, CBOR, Text, Binary)
- Viper integration (decode hook with multiple types)
- Edge cases (maximum values, zero, negative inputs)
Quick Start
# Install Ginkgo CLI (optional)
go install github.com/onsi/ginkgo/v2/ginkgo@latest
# Run all tests
go test ./...
# Run with coverage
go test -cover ./...
# Run with race detection (recommended)
CGO_ENABLED=1 go test -race ./...
# Using Ginkgo CLI
ginkgo -cover -race
Test Framework
Ginkgo v2 - BDD testing framework (docs)
- Hierarchical test organization (
Describe,Context,It) - Setup/teardown hooks (
BeforeEach,AfterEach,BeforeSuite,AfterSuite) - Parallel execution support
- Rich CLI with filtering
Gomega - Matcher library (docs)
- Readable assertion syntax
- Extensive built-in matchers
- Detailed failure messages
Running Tests
Basic Commands
# Standard test run
go test ./...
# Verbose output
go test -v ./...
# With coverage
go test -cover ./...
# Generate HTML coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
Expected Output:
=== RUN TestSize
Running Suite: size Suite - /sources/go/src/github.com/nabbar/golib/size
========================================================================
Random Seed: 1763323372
Will run 352 of 352 specs
••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
Ran 352 of 352 Specs in 0.015 seconds
SUCCESS! -- 352 Passed | 0 Failed | 0 Pending | 0 Skipped
--- PASS: TestSize (0.02s)
PASS
coverage: 95.4% of statements
ok github.com/nabbar/golib/size 0.026s coverage: 95.4% of statements
Ginkgo CLI Options
# Run all tests
ginkgo
# Specific test file
ginkgo --focus-file=parsing_test.go
# Pattern matching
ginkgo --focus="Parse"
# Parallel execution
ginkgo -p
# JUnit report
ginkgo --junit-report=results.xml
Race Detection
Critical for validating thread-safe operations
# Enable race detector (requires CGO)
CGO_ENABLED=1 go test -race ./...
# With timeout for long tests
CGO_ENABLED=1 go test -race -timeout=10m ./...
# With Ginkgo
CGO_ENABLED=1 ginkgo -race
Expected Output:
=== RUN TestSize
Running Suite: size Suite - /sources/go/src/github.com/nabbar/golib/size
========================================================================
Random Seed: 1763323372
Will run 352 of 352 specs
••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
Ran 352 of 352 Specs in 0.122 seconds
SUCCESS! -- 352 Passed | 0 Failed | 0 Pending | 0 Skipped
--- PASS: TestSize (0.14s)
PASS
coverage: 95.4% of statements
ok github.com/nabbar/golib/size 1.169s coverage: 95.4% of statements
Status: Zero data races detected across all 352 specs
Performance & Profiling
# Benchmarks
go test -bench=. -benchmem ./...
# Memory profiling
go test -memprofile=mem.out ./...
go tool pprof mem.out
# CPU profiling
go test -cpuprofile=cpu.out ./...
go tool pprof cpu.out
Performance Expectations
| Test Type | Duration | Notes |
|---|---|---|
| Full Suite | ~0.026s | Without race |
With -race |
~1.169s | 45x slower (normal for race detector) |
| Individual Spec | <1ms | Most tests |
| Complex Parsing | 1-2ms | Multiple unit components |
Test Coverage
Target: ≥95% statement coverage
Achieved: 95.4%
Coverage By Category
| Category | Test File | Specs | Description |
|---|---|---|---|
| Constants | constants_defaults_test.go |
~40 | Size constants, unit relationships, defaults |
| Parsing | parsing_test.go |
~70 | String parsing, unit detection, edge cases |
| Formatting | formatting_test.go |
~60 | String formatting, precision, unit selection |
| Arithmetic | arithmetic_operations_test.go |
~50 | Math operations, overflow/underflow protection |
| Conversions | type_conversions_test.go |
~60 | Type conversions with overflow detection |
| Encoding | encoding_marshalling_test.go |
~50 | JSON, YAML, TOML, CBOR, Text, Binary |
| Viper | viper_decoder_test.go |
~22 | Viper decode hook, multiple input types |
Detailed Coverage
# Generate coverage report
go test -coverprofile=coverage.out ./...
# View in terminal (by function)
go tool cover -func=coverage.out
# View HTML report (by line)
go tool cover -html=coverage.out -o coverage.html
Coverage by File:
github.com/nabbar/golib/size/arithmetic.go:39: Mul 100.0%
github.com/nabbar/golib/size/arithmetic.go:51: MulErr 100.0%
github.com/nabbar/golib/size/arithmetic.go:65: Div 100.0%
github.com/nabbar/golib/size/arithmetic.go:75: DivErr 100.0%
github.com/nabbar/golib/size/arithmetic.go:88: Add 100.0%
github.com/nabbar/golib/size/arithmetic.go:100: AddErr 100.0%
github.com/nabbar/golib/size/arithmetic.go:114: Sub 100.0%
github.com/nabbar/golib/size/arithmetic.go:125: SubErr 100.0%
github.com/nabbar/golib/size/encode.go:49: MarshalJSON 100.0%
github.com/nabbar/golib/size/encode.go:63: UnmarshalJSON 100.0%
github.com/nabbar/golib/size/encode.go:77: MarshalYAML 100.0%
github.com/nabbar/golib/size/encode.go:91: UnmarshalYAML 100.0%
github.com/nabbar/golib/size/format.go:73: String 100.0%
github.com/nabbar/golib/size/format.go:92: Int64 100.0%
github.com/nabbar/golib/size/format.go:110: Int32 100.0%
github.com/nabbar/golib/size/format.go:128: Int 100.0%
github.com/nabbar/golib/size/parse.go:90: parseBytes 100.0%
github.com/nabbar/golib/size/parse.go:115: parseString 95.8%
total: (statements) 95.4%
Thread Safety
Validation
The size package is validated for thread safety using Go's race detector:
# Run all tests with race detection
CGO_ENABLED=1 go test -race -timeout=10m -v -cover -covermode=atomic ./...
Thread Safety Guarantees:
- ✅ Value Type: Size is a simple
uint64wrapper, safe to copy - ✅ Concurrent Reads: Multiple goroutines can safely read Size values
- ✅ Concurrent Writes: Pointer receiver methods (
Mul,Add, etc.) require external synchronization - ✅ Parse/Format: Stateless operations, safe for concurrent use
- ✅ Marshaling: No shared state, thread-safe
Concurrent Usage Patterns
Safe Pattern - Value semantics:
func processFiles(files []string) {
var wg sync.WaitGroup
for _, file := range files {
wg.Add(1)
go func(f string) {
defer wg.Done()
// Each goroutine works with its own Size value
fileSize, _ := size.Parse(f.Size())
fmt.Println(fileSize.String()) // Safe
}(file)
}
wg.Wait()
}
Unsafe Pattern - Shared mutable state:
func accumulateSizesBad(sizes []size.Size) size.Size {
total := size.SizeNul
var wg sync.WaitGroup
for _, s := range sizes {
wg.Add(1)
go func(sz size.Size) {
defer wg.Done()
total.Add(sz.Uint64()) // ❌ RACE CONDITION
}(s)
}
wg.Wait()
return total
}
Safe Pattern - Synchronized writes:
func accumulateSizesGood(sizes []size.Size) size.Size {
total := size.SizeNul
var mu sync.Mutex
var wg sync.WaitGroup
for _, s := range sizes {
wg.Add(1)
go func(sz size.Size) {
defer wg.Done()
mu.Lock()
total.Add(sz.Uint64()) // ✅ Protected
mu.Unlock()
}(s)
}
wg.Wait()
return total
}
Writing Tests
Test Template
package size_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/nabbar/golib/size"
)
var _ = Describe("Feature Name", func() {
Context("when condition X", func() {
It("should behave correctly", func() {
// Arrange
input := "10MB"
// Act
result, err := size.Parse(input)
// Assert
Expect(err).ToNot(HaveOccurred())
Expect(result.MegaBytes()).To(Equal(uint64(10)))
})
})
Context("when edge case Y", func() {
It("should handle gracefully", func() {
// Test edge case
result, err := size.Parse("0")
Expect(err).ToNot(HaveOccurred())
Expect(result).To(Equal(size.SizeNul))
})
})
})
Key Testing Patterns
1. Parsing Tests
It("should parse various formats", func() {
testCases := []struct {
input string
expected uint64
}{
{"1024", 1024},
{"1KB", 1024},
{"1.5MB", 1572864},
{"1GB500MB", 1610612736},
}
for _, tc := range testCases {
s, err := size.Parse(tc.input)
Expect(err).ToNot(HaveOccurred(), "input: %s", tc.input)
Expect(s.Uint64()).To(Equal(tc.expected), "input: %s", tc.input)
}
})
2. Arithmetic Tests
It("should detect overflow", func() {
s := size.Size(math.MaxUint64 / 2)
err := s.MulErr(3.0)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("overflow"))
})
3. Marshaling Tests
It("should round-trip through JSON", func() {
original := size.ParseUint64(10485760)
data, err := json.Marshal(original)
Expect(err).ToNot(HaveOccurred())
var decoded size.Size
err = json.Unmarshal(data, &decoded)
Expect(err).ToNot(HaveOccurred())
Expect(decoded).To(Equal(original))
})
Best Practices
Test Organization
DO: Group related tests
Describe("Size Parsing", func() {
Context("with valid input", func() {
It("should parse simple units", func() { /* ... */ })
It("should parse complex expressions", func() { /* ... */ })
})
Context("with invalid input", func() {
It("should return error for empty string", func() { /* ... */ })
It("should return error for unknown unit", func() { /* ... */ })
})
})
DON'T: Mix unrelated tests
It("should do everything", func() {
// Parsing test
s, _ := size.Parse("10MB")
// Arithmetic test
s.Add(1024)
// Formatting test
_ = s.String()
// Too many concerns in one test
})
Error Testing
DO: Test specific error conditions
It("should return error for negative size", func() {
_, err := size.Parse("-10MB")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("negative"))
})
DON'T: Ignore error details
It("should error on bad input", func() {
_, err := size.Parse("bad")
Expect(err).To(HaveOccurred()) // Too vague
})
Table-Driven Tests
DO: Use tables for multiple similar cases
DescribeTable("parsing various formats",
func(input string, expected uint64) {
s, err := size.Parse(input)
Expect(err).ToNot(HaveOccurred())
Expect(s.Uint64()).To(Equal(expected))
},
Entry("bytes", "1024", uint64(1024)),
Entry("kilobytes", "1KB", uint64(1024)),
Entry("megabytes", "1MB", uint64(1048576)),
Entry("decimal", "1.5MB", uint64(1572864)),
)
Coverage Goals
- New Features: 100% coverage
- Bug Fixes: Add regression test
- Edge Cases: Test boundaries (0, max, overflow)
- Error Paths: Test all error returns
Troubleshooting
Common Issues
Problem: Tests fail with "undefined: Size"
# Solution: Check imports
import (
"github.com/nabbar/golib/size"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
Problem: Race detector reports issues
# Solution: Review concurrent access patterns
# Use mutex for shared mutable state
var mu sync.Mutex
mu.Lock()
sharedSize.Add(value)
mu.Unlock()
Problem: Coverage lower than expected
# Solution: Check which lines are not covered
go test -coverprofile=coverage.out
go tool cover -func=coverage.out
# Look for uncovered lines and add tests
Debugging Tests
Verbose Output:
go test -v ./...
ginkgo -v --trace
Focus on Failing Test:
ginkgo --focus="specific test name"
Step-by-Step Debugging:
It("should work", func() {
GinkgoWriter.Println("Debug: input =", input)
result, err := size.Parse(input)
GinkgoWriter.Printf("Debug: result = %v, err = %v\n", result, err)
Expect(err).ToNot(HaveOccurred())
})
CI Integration
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run tests
run: go test -timeout=10m -v -cover -covermode=atomic ./...
- name: Run race detector
run: CGO_ENABLED=1 go test -race -timeout=10m -v ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.out
GitLab CI
test:size:
stage: test
script:
- cd size
- go test -timeout=10m -v -cover -covermode=atomic ./...
- CGO_ENABLED=1 go test -race -timeout=10m -v ./...
coverage: '/coverage: (\d+\.\d+)% of statements/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
Pre-commit Hook
#!/bin/bash
# .git/hooks/pre-commit
echo "Running tests..."
go test ./... || exit 1
echo "Running race detector..."
CGO_ENABLED=1 go test -race ./... || exit 1
echo "Checking coverage..."
go test -cover ./... | grep "coverage:" | awk '{if ($2 < 95.0) exit 1}'
AI Transparency Notice
In accordance with Article 50.4 of the EU AI Act, AI assistance has been used for testing, documentation, and bug fixing under human supervision.
Resources
- Ginkgo Docs: https://onsi.github.io/ginkgo/
- Gomega Docs: https://onsi.github.io/gomega/
- Go Testing: https://golang.org/pkg/testing/
- Race Detector: https://go.dev/doc/articles/race_detector
- Coverage Tool: https://go.dev/blog/cover