mirror of
https://github.com/nabbar/golib.git
synced 2025-12-24 11:51:02 +08:00
[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
1013 lines
24 KiB
Markdown
1013 lines
24 KiB
Markdown
# Testing Guide
|
|
|
|
[](https://opensource.org/licenses/MIT)
|
|
[](https://golang.org/)
|
|
[]()
|
|
[]()
|
|
|
|
Comprehensive testing documentation for the mail/render package, covering test execution, benchmarks, and quality assurance.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
- [Overview](#overview)
|
|
- [Quick Start](#quick-start)
|
|
- [Test Framework](#test-framework)
|
|
- [Running Tests](#running-tests)
|
|
- [Test Coverage](#test-coverage)
|
|
- [Thread Safety](#thread-safety)
|
|
- [Benchmarks](#benchmarks)
|
|
- [Writing Tests](#writing-tests)
|
|
- [Best Practices](#best-practices)
|
|
- [Troubleshooting](#troubleshooting)
|
|
- [CI Integration](#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
|
|
|
|
```bash
|
|
# 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](https://onsi.github.io/ginkgo/))
|
|
- 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](https://onsi.github.io/gomega/))
|
|
- Readable assertion syntax
|
|
- Extensive built-in matchers
|
|
- Detailed failure messages
|
|
- Asynchronous assertions
|
|
|
|
### gmeasure
|
|
|
|
**Performance measurement** ([docs](https://onsi.github.io/gomega/#gmeasure-benchmarking-code))
|
|
- Statistical measurements (mean, median, stddev, max)
|
|
- Structured benchmark reporting
|
|
- Multiple measurement types
|
|
- Integration with Ginkgo Report Entries
|
|
|
|
---
|
|
|
|
## Running Tests
|
|
|
|
### Basic Commands
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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**
|
|
|
|
```bash
|
|
# 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**:
|
|
```bash
|
|
# ✅ 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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```go
|
|
// 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**:
|
|
```go
|
|
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**:
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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, validation
|
|
- `concurrency_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:
|
|
|
|
```go
|
|
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**
|
|
```go
|
|
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)
|
|
```go
|
|
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**
|
|
```go
|
|
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**
|
|
```go
|
|
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**
|
|
```go
|
|
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**
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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 `BeforeEach` for setup, `AfterEach` for cleanup
|
|
- Avoid global mutable state
|
|
- Create test data on-demand
|
|
- Don't rely on test execution order
|
|
|
|
```go
|
|
BeforeEach(func() {
|
|
mailer = render.New()
|
|
// Fresh instance for each test
|
|
})
|
|
```
|
|
|
|
**❌ Bad Practices**:
|
|
```go
|
|
// 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**:
|
|
```go
|
|
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**:
|
|
```go
|
|
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**:
|
|
```go
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(value).To(Equal(expected))
|
|
Expect(buf.Len()).To(BeNumerically(">", 1000))
|
|
Expect(html).To(ContainSubstring("<html>"))
|
|
```
|
|
|
|
**❌ Generic Comparisons**:
|
|
```go
|
|
Expect(value == expected).To(BeTrue()) // Less informative
|
|
Expect(err == nil).To(BeTrue()) // Use HaveOccurred()
|
|
```
|
|
|
|
### Performance Testing
|
|
|
|
**✅ Use gmeasure**:
|
|
```go
|
|
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**:
|
|
```go
|
|
AfterEach(func() {
|
|
// Clean up if needed
|
|
mailer = nil
|
|
})
|
|
```
|
|
|
|
**✅ Defer in Tests**:
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
# Clean test cache
|
|
go clean -testcache
|
|
|
|
# Run tests fresh
|
|
go test -count=1 ./...
|
|
```
|
|
|
|
### Race Conditions
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```go
|
|
package render_test
|
|
|
|
import (
|
|
"testing"
|
|
"github.com/nabbar/golib/mail/render"
|
|
)
|
|
```
|
|
|
|
### CGO Not Available
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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**:
|
|
```go
|
|
It("should do something", func() {
|
|
fmt.Fprintf(GinkgoWriter, "Debug: mailer = %+v\n", mailer)
|
|
// Test code...
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## CI Integration
|
|
|
|
### GitHub Actions Example
|
|
|
|
```yaml
|
|
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`:
|
|
|
|
```bash
|
|
#!/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:
|
|
```bash
|
|
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
|
|
- [Ginkgo Documentation](https://onsi.github.io/ginkgo/)
|
|
- [Gomega Matchers](https://onsi.github.io/gomega/)
|
|
- [gmeasure Guide](https://onsi.github.io/gomega/#gmeasure-benchmarking-code)
|
|
- [Go Testing](https://pkg.go.dev/testing)
|
|
|
|
### Concurrency
|
|
- [Go Race Detector](https://go.dev/doc/articles/race_detector)
|
|
- [Go Memory Model](https://go.dev/ref/mem)
|
|
- [sync Package](https://pkg.go.dev/sync)
|
|
|
|
### Benchmarking
|
|
- [Go Benchmarks](https://pkg.go.dev/testing#hdr-Benchmarks)
|
|
- [Performance Profiling](https://go.dev/blog/pprof)
|
|
|
|
### Related Documentation
|
|
- [README.md](README.md) - Package overview
|
|
- [GoDoc](https://pkg.go.dev/github.com/nabbar/golib/mail/render) - API 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
|