[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
24 KiB
Testing Guide
Comprehensive testing documentation for the mail/render package, covering test execution, benchmarks, and quality assurance.
Table of Contents
- Overview
- Quick Start
- Test Framework
- Running Tests
- Test Coverage
- Thread Safety
- Benchmarks
- Writing Tests
- Best Practices
- Troubleshooting
- CI Integration
Overview
The mail/render package uses Ginkgo v2 (BDD testing framework), Gomega (matcher library), and gmeasure (performance measurement) for comprehensive testing with expressive assertions and accurate benchmarks.
Test Suite Metrics
- Total Specs: 123
- Coverage: 89.6% of statements
- Race Detection: ✅ Zero data races
- Execution Time: ~1.7s (without race), ~29.6s (with race)
- Test Files: 7 organized test files
Coverage Areas
- Mailer interface and configuration
- Theme and text direction parsing
- Email body management and cloning
- HTML and plain text generation
- Template variable replacement (ParseData)
- Concurrent operations and thread safety
- Error handling and validation
Quick Start
# Install Ginkgo CLI (optional but recommended)
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 ./...
# Run benchmarks
go test -v ./... | grep -A 10 "Report Entries"
# Using Ginkgo CLI
ginkgo -v
ginkgo -cover -race
Expected Output:
Ran 123 of 123 Specs in 1.747 seconds
SUCCESS! -- 123 Passed | 0 Failed | 0 Pending | 0 Skipped
coverage: 89.6% of statements
Test Framework
Ginkgo v2
BDD testing framework (docs)
- Hierarchical test organization (
Describe,Context,It) - Setup/teardown hooks (
BeforeEach,AfterEach) - Expressive spec descriptions
- Rich failure reporting
- Built-in parallelization support
Gomega
Matcher library (docs)
- Readable assertion syntax
- Extensive built-in matchers
- Detailed failure messages
- Asynchronous assertions
gmeasure
Performance measurement (docs)
- Statistical measurements (mean, median, stddev, max)
- Structured benchmark reporting
- Multiple measurement types
- Integration with Ginkgo Report Entries
Running Tests
Basic Commands
# Standard test run
go test ./...
# Verbose output
go test -v ./...
# With coverage report
go test -cover ./...
# Coverage with profile
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Specific timeout
go test -timeout=10m ./...
Ginkgo CLI Options
# Run all tests
ginkgo
# Verbose output
ginkgo -v
# Focus on specific test
ginkgo --focus="should generate HTML"
# Focus on file
ginkgo --focus-file=render_test.go
# Parallel execution
ginkgo -p
# With coverage
ginkgo -cover -coverprofile=coverage.out
# Generate JUnit report
ginkgo --junit-report=results.xml
Race Detection
Critical for concurrent operations testing
# Enable race detector (requires CGO)
CGO_ENABLED=1 go test -race ./...
# With Ginkgo
CGO_ENABLED=1 ginkgo -race
# Verbose race detection
CGO_ENABLED=1 go test -race -v ./...
What Race Detection Validates:
- Clone() method creates independent copies
- No shared references in deep copy
- Thread-safe concurrent generation
- Proper goroutine synchronization
Expected Output:
# ✅ Success (Zero races)
Ran 123 of 123 Specs in 29.554 seconds
SUCCESS! -- 123 Passed | 0 Failed | 0 Pending | 0 Skipped
coverage: 89.6% of statements
ok github.com/nabbar/golib/mail/render 30.634s
# ❌ Race detected (should not happen)
==================
WARNING: DATA RACE
Read at 0x... by goroutine ...
Previous write at 0x... by goroutine ...
==================
Status: Zero data races detected across all test scenarios
Performance Impact
| Test Mode | Duration | Overhead | Notes |
|---|---|---|---|
| Normal | ~1.7s | 1x | Standard execution |
| Race Detection | ~29.6s | 17x | Expected overhead |
| Coverage | ~1.8s | 1.06x | Minimal overhead |
Race detection overhead is normal due to instrumentation
Test Coverage
Current Coverage: 89.6%
Coverage by File:
| File | Coverage | Lines | Description |
|---|---|---|---|
interface.go |
~95% | 167 | Mailer interface and Clone() |
email.go |
100% | 105 | Getters and setters |
config.go |
~90% | 85 | Configuration and validation |
render.go |
~85% | 120 | HTML/text generation, ParseData |
themes.go |
100% | 66 | Theme parsing and conversion |
direction.go |
~95% | 88 | Text direction parsing |
error.go |
100% | 63 | Error codes and messages |
Uncovered Areas (10.4%):
- Edge cases in error handling
- Rare validation failure paths
- Default fallback code (theme/direction)
View Coverage Details
# Generate coverage report
go test -coverprofile=coverage.out ./...
# View in terminal (function-level)
go tool cover -func=coverage.out
# Generate HTML report
go tool cover -html=coverage.out -o coverage.html
open coverage.html # macOS
xdg-open coverage.html # Linux
Coverage Example Output
github.com/nabbar/golib/mail/render/config.go:49: Validate 90.0%
github.com/nabbar/golib/mail/render/direction.go:63: ParseTextDirection 95.2%
github.com/nabbar/golib/mail/render/email.go:38: SetTheme 100.0%
github.com/nabbar/golib/mail/render/interface.go:86: Clone 94.7%
github.com/nabbar/golib/mail/render/render.go:36: ParseData 85.4%
github.com/nabbar/golib/mail/render/themes.go:56: ParseTheme 100.0%
total: (statements) 89.6%
Thread Safety
Thread safety is critical for concurrent email generation using Clone().
Concurrency Primitives
The package uses deep copying to ensure thread safety:
// Clone() performs deep copy of:
// - Slices (Intros, Outros, Dictionary)
// - Tables with nested data
// - Actions and buttons
// - Maps (Table columns)
Verified Components
| Component | Mechanism | Concurrent Ops | Status |
|---|---|---|---|
Clone() |
Deep copy of all nested structures | ✅ Independent copies | Race-free |
ParseData() |
In-place modification | ❌ Not thread-safe | Use per-clone |
GenerateHTML() |
Stateless rendering | ✅ Via Clone() | Race-free |
Config.NewMailer() |
Immutable creation | ✅ No shared state | Race-free |
Thread Safety Pattern
✅ Correct Pattern:
baseMailer := render.New()
// Configure base template...
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
mailer := baseMailer.Clone() // Independent copy
// Safe to modify mailer in this goroutine
body := &hermes.Body{Name: fmt.Sprintf("User %d", id)}
mailer.SetBody(body)
htmlBuf, _ := mailer.GenerateHTML()
}(i)
}
wg.Wait()
❌ Incorrect Pattern:
mailer := render.New()
for i := 0; i < 100; i++ {
go func(id int) {
// RACE CONDITION: Shared mailer
body := &hermes.Body{Name: fmt.Sprintf("User %d", id)}
mailer.SetBody(body) // Concurrent writes!
mailer.GenerateHTML()
}(i)
}
Testing Commands
# Full suite with race detection
CGO_ENABLED=1 go test -race -v ./...
# Focus on concurrency tests
CGO_ENABLED=1 go test -race -v -run "Concurrency" ./...
# Stress test (run multiple times)
for i in {1..10}; do
echo "Run $i"
CGO_ENABLED=1 go test -race ./... || break
done
Result: Zero data races across all concurrent test scenarios
Benchmarks
Benchmark Results
All benchmarks use gmeasure for statistical accuracy with 100-1000 iterations.
Creation Benchmarks
| Operation | Mean | Median | StdDev | Notes |
|---|---|---|---|---|
New() |
~100 ns | ~100 ns | ~20 ns | Struct initialization |
Config.NewMailer() |
~300 ns | ~300 ns | ~50 ns | With parsing |
Clone Benchmarks
| Operation | Mean | Median | StdDev | Notes |
|---|---|---|---|---|
| Simple Clone | ~1 µs | ~1 µs | ~200 ns | Empty body |
| Complex Clone | ~10 µs | ~10 µs | ~2 µs | Tables + actions |
Generation Benchmarks (without race)
| Operation | Mean | Median | StdDev | Max | Notes |
|---|---|---|---|---|---|
| HTML (Simple) | 2.7 ms | 2.5 ms | 300 µs | 3.4 ms | Basic email |
| HTML (Complex) | 3.1 ms | 3.0 ms | 400 µs | 4.7 ms | Tables + actions |
| Plain Text | 3.6 ms | 3.5 ms | 400 µs | 5.6 ms | Text conversion |
Generation Benchmarks (with race detection)
| Operation | Mean | Median | StdDev | Max | Notes |
|---|---|---|---|---|---|
| HTML (Simple) | 43 ms | 41 ms | 2.5 ms | 52 ms | 16x overhead |
| HTML (Complex) | 51 ms | 50 ms | 3.7 ms | 66 ms | 16x overhead |
| Plain Text | 48 ms | 47 ms | 3.2 ms | 60 ms | 13x overhead |
Race detection overhead is expected and acceptable for testing
ParseData Benchmarks
| Operation | Mean | Median | Notes |
|---|---|---|---|
| Simple (few variables) | 350 ns | 300 ns | ~5 replacements |
| Complex (many variables) | 1.2 µs | 1.0 µs | ~20 replacements |
Parsing Benchmarks
| Operation | Mean | Median | Notes |
|---|---|---|---|
ParseTheme() |
83 ns | 70 ns | String comparison |
ParseTextDirection() |
54 ns | 50 ns | String parsing |
Validation Benchmarks
| Operation | Mean | Median | StdDev | Notes |
|---|---|---|---|---|
Config.Validate() |
31 µs | 30 µs | 5 µs | All validations |
Complete Workflow Benchmark
| Operation | Mean | Median | StdDev | Max | Notes |
|---|---|---|---|---|---|
| Config → HTML | 3.1 ms | 3.0 ms | 400 µs | 4.7 ms | Full pipeline |
Workflow: Config validation → NewMailer() → ParseData → GenerateHTML
Running Benchmarks
# Run all benchmarks
go test -v ./... 2>&1 | grep -A 20 "Report Entries"
# Save benchmark results
go test -v ./... > benchmark-results.txt
# Compare benchmarks
go test -bench=. -benchmem ./...
Benchmark Test Files
Benchmarks are integrated into test files using gmeasure:
benchmark_test.go- Creation, generation, parsing, validationconcurrency_test.go- Concurrent operations
Performance Expectations
Target Performance:
- Email generation: <5ms (99th percentile)
- Clone operation: <20µs (complex)
- ParseData: <2µs (complex)
- Config validation: <50µs
Throughput Estimates (single-threaded):
- Email generation: ~300-350 emails/second
- With 10 goroutines: ~3000 emails/second
- ParseData only: ~1M operations/second
Test File Organization
Test Suite Structure
| File | Specs | Purpose | Focus |
|---|---|---|---|
render_suite_test.go |
1 | Suite initialization | Ginkgo entry point |
interface_test.go |
28 | Mailer interface | CRUD, Clone, getters/setters |
config_test.go |
20 | Configuration | Validation, NewMailer |
themes_test.go |
21 | Themes & direction | Parsing, string conversion |
render_test.go |
38 | Email generation | HTML, text, ParseData |
concurrency_test.go |
5 | Thread safety | Concurrent operations |
benchmark_test.go |
10 | Performance | gmeasure benchmarks |
Total: 123 test specifications
Test Organization Pattern
Tests follow Ginkgo's hierarchical BDD structure:
var _ = Describe("Component", func() {
Context("When doing operation", func() {
var (
testData []byte
mailer render.Mailer
)
BeforeEach(func() {
// Per-test setup
mailer = render.New()
})
It("should perform expected behavior", func() {
// Arrange
mailer.SetName("Test Company")
// Act
result, err := mailer.GenerateHTML()
// Assert
Expect(err).ToNot(HaveOccurred())
Expect(result.Len()).To(BeNumerically(">", 0))
})
})
})
Writing Tests
Test Guidelines
1. Descriptive Names
It("should generate HTML email with proper structure", func() {
// Clear, specific description
})
It("should handle empty body gracefully", func() {
// Describes the edge case
})
2. Follow AAA Pattern (Arrange, Act, Assert)
It("should replace template variables in all fields", func() {
// Arrange
mailer := render.New()
mailer.SetName("{{company}}")
body := &hermes.Body{
Name: "{{user}}",
Intros: []string{"Code: {{code}}"},
}
mailer.SetBody(body)
// Act
mailer.ParseData(map[string]string{
"{{company}}": "Acme Inc",
"{{user}}": "John Doe",
"{{code}}": "123456",
})
// Assert
result := mailer.GetName()
Expect(result).To(Equal("Acme Inc"))
body = mailer.GetBody()
Expect(body.Name).To(Equal("John Doe"))
Expect(body.Intros[0]).To(Equal("Code: 123456"))
})
3. Use Appropriate Matchers
Expect(value).To(Equal(expected)) // Exact match
Expect(err).ToNot(HaveOccurred()) // No error
Expect(list).To(ContainElement(item)) // Contains
Expect(number).To(BeNumerically(">", 0)) // Numeric comparison
Expect(str).To(ContainSubstring("text")) // Substring
Expect(buf.Len()).To(BeNumerically(">", 1000)) // Buffer length
4. Test Error Cases
It("should return error for invalid configuration", func() {
config := render.Config{
Name: "", // Missing required field
Link: "not-a-url", // Invalid URL
}
err := config.Validate()
Expect(err).To(HaveOccurred())
Expect(err.Code()).To(Equal(render.ErrorMailerConfigInvalid))
})
5. Test Edge Cases
It("should handle empty body content", func() {
mailer := render.New()
body := &hermes.Body{} // Empty
mailer.SetBody(body)
htmlBuf, err := mailer.GenerateHTML()
Expect(err).ToNot(HaveOccurred())
Expect(htmlBuf.Len()).To(BeNumerically(">", 0))
})
It("should handle special characters in content", func() {
mailer := render.New()
body := &hermes.Body{
Name: "Test & <User>",
Intros: []string{"Special chars: & < > \" '"},
}
mailer.SetBody(body)
htmlBuf, _ := mailer.GenerateHTML()
html := htmlBuf.String()
Expect(html).To(ContainSubstring("&"))
Expect(html).To(ContainSubstring("<"))
})
6. Test Concurrent Operations
It("should safely generate emails concurrently", func() {
baseMailer := render.New()
baseMailer.SetName("Test Company")
var wg sync.WaitGroup
errors := make(chan error, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
mailer := baseMailer.Clone()
body := &hermes.Body{Name: fmt.Sprintf("User %d", id)}
mailer.SetBody(body)
_, err := mailer.GenerateHTML()
if err != nil {
errors <- err
}
}(i)
}
wg.Wait()
close(errors)
Expect(errors).To(BeEmpty())
})
Test Template
var _ = Describe("render/new_feature", func() {
Context("When using new feature", func() {
var (
mailer render.Mailer
)
BeforeEach(func() {
mailer = render.New()
// Setup common state
})
It("should perform expected behavior", func() {
// Arrange
mailer.SetTheme(render.ThemeFlat)
// Act
theme := mailer.GetTheme()
// Assert
Expect(theme).To(Equal(render.ThemeFlat))
})
It("should handle error case", func() {
// Test error conditions
config := render.Config{}
err := config.Validate()
Expect(err).To(HaveOccurred())
})
It("should work with edge cases", func() {
// Test boundary conditions
})
})
})
Best Practices
Test Independence
✅ Good Practices:
- Each test should be independent
- Use
BeforeEachfor setup,AfterEachfor cleanup - Avoid global mutable state
- Create test data on-demand
- Don't rely on test execution order
BeforeEach(func() {
mailer = render.New()
// Fresh instance for each test
})
❌ Bad Practices:
// Don't share state across tests
var sharedMailer render.Mailer
It("test 1", func() {
sharedMailer.SetName("Test") // Affects other tests
})
It("test 2", func() {
name := sharedMailer.GetName() // Depends on test 1
})
Test Data
✅ Use Realistic Data:
body := &hermes.Body{
Name: "John Doe",
Intros: []string{"Welcome to our service!"},
Dictionary: []hermes.Entry{
{Key: "Order ID", Value: "ORD-123456"},
{Key: "Date", Value: "2024-01-15"},
},
}
✅ Test Data Helpers:
func createTestBody() *hermes.Body {
return &hermes.Body{
Name: "Test User",
Intros: []string{"Test intro"},
}
}
func createTestConfig() render.Config {
return render.Config{
Theme: "flat",
Direction: "ltr",
Name: "Test Company",
Link: "https://example.com",
Logo: "https://example.com/logo.png",
Copyright: "© 2024 Test",
TroubleText: "Help",
Body: hermes.Body{},
}
}
Assertions
✅ Specific Matchers:
Expect(err).ToNot(HaveOccurred())
Expect(value).To(Equal(expected))
Expect(buf.Len()).To(BeNumerically(">", 1000))
Expect(html).To(ContainSubstring("<html>"))
❌ Generic Comparisons:
Expect(value == expected).To(BeTrue()) // Less informative
Expect(err == nil).To(BeTrue()) // Use HaveOccurred()
Performance Testing
✅ Use gmeasure:
It("should measure generation performance", func() {
experiment := gmeasure.NewExperiment("generation")
AddReportEntry(experiment.Name, experiment)
for i := 0; i < 100; i++ {
experiment.Sample(func(idx int) {
mailer := render.New()
body := createTestBody()
mailer.SetBody(body)
_, _ = mailer.GenerateHTML()
}, gmeasure.SamplingConfig{N: 1})
}
stats := experiment.GetStats("generation")
AddReportEntry("Mean time", stats.DurationFor(gmeasure.StatMean))
})
Cleanup
✅ Explicit Cleanup:
AfterEach(func() {
// Clean up if needed
mailer = nil
})
✅ Defer in Tests:
It("should cleanup resources", func() {
file, err := os.CreateTemp("", "test-*.html")
Expect(err).ToNot(HaveOccurred())
defer os.Remove(file.Name())
// Use file...
})
Troubleshooting
Stale Test Cache
# Clean test cache
go clean -testcache
# Run tests fresh
go test -count=1 ./...
Race Conditions
# Debug races with verbose output
CGO_ENABLED=1 go test -race -v ./... 2>&1 | tee race-log.txt
# Search for race warnings
grep -A 30 "WARNING: DATA RACE" race-log.txt
Common Race Patterns:
- Shared Mailer across goroutines (use Clone())
- Concurrent ParseData calls (clone first)
- Shared configuration mutation
Test Timeouts
# Increase timeout
go test -timeout=15m ./...
# Identify slow tests
go test -v ./... | grep -E "^--- (PASS|FAIL):"
Import Cycles
Use package render_test to avoid import cycles:
package render_test
import (
"testing"
"github.com/nabbar/golib/mail/render"
)
CGO Not Available
# Install build tools
# Ubuntu/Debian:
sudo apt-get install build-essential
# macOS:
xcode-select --install
# Verify
export CGO_ENABLED=1
go test -race ./...
Debugging Failed Tests
# Run specific test
ginkgo --focus="should generate HTML"
# Verbose output
ginkgo -v --trace
# Stop on first failure
ginkgo --fail-fast
Use GinkgoWriter for debug output:
It("should do something", func() {
fmt.Fprintf(GinkgoWriter, "Debug: mailer = %+v\n", mailer)
// Test code...
})
CI Integration
GitHub Actions Example
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.20', '1.21']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Install dependencies
run: go mod download
- name: Run tests
run: go test -v -timeout=10m ./...
- name: Race detection
run: CGO_ENABLED=1 go test -race -timeout=10m ./...
- name: Coverage
run: |
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.out
Pre-commit Hook
Save as .git/hooks/pre-commit:
#!/bin/bash
set -e
echo "Running tests..."
go test -timeout=5m ./mail/render/...
echo "Running race detection..."
CGO_ENABLED=1 go test -race -timeout=5m ./mail/render/...
echo "Checking coverage..."
go test -cover ./mail/render/... | grep -E "coverage:" | awk '{if ($2 < 85.0) exit 1}'
echo "✅ All checks passed"
Make executable:
chmod +x .git/hooks/pre-commit
Quality Checklist
Before merging code:
- All tests pass:
go test ./... - Race detection clean:
CGO_ENABLED=1 go test -race ./... - Coverage maintained: ≥85% (currently 89.6%)
- New features have tests
- Error cases tested
- Thread safety validated (if applicable)
- Benchmarks run successfully
- Documentation updated
- Test duration reasonable (<5s without race)
Performance Targets
Test Execution Targets
| Metric | Target | Current | Status |
|---|---|---|---|
| Total specs | - | 123 | ✅ |
| Execution time | <3s | 1.7s | ✅ |
| Execution time (race) | <45s | 29.6s | ✅ |
| Coverage | ≥85% | 89.6% | ✅ |
| Data races | 0 | 0 | ✅ |
Operation Performance Targets
| Operation | Target | Current | Status |
|---|---|---|---|
| Email generation | <5ms | 2.7-3.6ms | ✅ |
| Clone (complex) | <20µs | ~10µs | ✅ |
| ParseData | <5µs | 0.35-1.2µs | ✅ |
| Config validation | <100µs | ~31µs | ✅ |
Resources
Testing Frameworks
Concurrency
Benchmarking
Related Documentation
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.
Version: Go 1.18+ on Linux, macOS, Windows
Maintained By: Mail Render Package Contributors