Files
golib/mail/render/TESTING.md
nabbar 25c3c8c45b Improvements, test & documentatons (2025-11 #2)
[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
2025-11-16 21:48:48 +01:00

24 KiB

Testing Guide

License: MIT Go Version Tests Coverage

Comprehensive testing documentation for the mail/render package, covering test execution, benchmarks, and quality assurance.


Table of Contents


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, 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:

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("&amp;"))
    Expect(html).To(ContainSubstring("&lt;"))
})

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 BeforeEach for setup, AfterEach for 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


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