Files
golib/mail/sender/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

20 KiB

Testing Guide

License: MIT Go Version Tests Coverage

Comprehensive testing documentation for the mail/sender package, covering test execution, race detection, and quality assurance.


Table of Contents


Overview

The mail/sender package uses Ginkgo v2 (BDD testing framework) and Gomega (matcher library) for comprehensive testing with expressive assertions and real SMTP server integration.

Test Suite Statistics

  • Total Specs: 252
  • Coverage: 81.4%
  • Race Detection: Zero data races
  • Execution Time: ~0.8s (without race), ~1.5s (with race)

Coverage Areas

  • Mail interface operations (creation, properties, headers)
  • Email address management (From, To, CC, BCC)
  • Body and attachment handling
  • Configuration validation and creation
  • Sender creation and lifecycle
  • Error handling and edge cases
  • Type parsing (encoding, priority, content type)
  • Real SMTP server integration for send operations
  • Performance benchmarks with gmeasure

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

Expected Output:

Ran 252 of 252 Specs in 0.808 seconds
SUCCESS! -- 252 Passed | 0 Failed | 0 Pending | 0 Skipped
PASS
coverage: 81.4% of statements
ok  	github.com/nabbar/golib/mail/sender	0.839s

Test Framework

Ginkgo v2 - BDD testing framework (docs)

  • Hierarchical test organization (Describe, Context, It)
  • Setup/teardown hooks (BeforeEach, AfterEach, BeforeSuite, AfterSuite)
  • Rich CLI with filtering and focus
  • Detailed failure reporting

Gomega - Matcher library (docs)

  • Readable assertion syntax
  • Extensive built-in matchers
  • Detailed failure messages
  • Custom matchers support

gmeasure - Performance benchmarking (docs)

  • Built on top of Gomega
  • Statistical analysis (mean, median, stddev)
  • Performance regression detection
  • Experiment-based benchmarking

Running Tests

Basic Commands

# Standard test run
go test ./...

# Verbose output
go test -v ./...

# With coverage
go test -cover ./...

# Specific timeout
go test -timeout=10m ./...

# Generate HTML coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

Ginkgo CLI Options

# Run all tests
ginkgo

# Specific test file
ginkgo --focus-file=mail_test.go

# Pattern matching
ginkgo --focus="Email address"

# Skip specific tests
ginkgo --skip="Performance"

# Parallel execution (use with caution - SMTP server conflicts)
ginkgo -p --procs=4

# JUnit report
ginkgo --junit-report=results.xml

# JSON report
ginkgo --json-report=results.json

Race Detection

Critical for validating thread-safety assumptions

# Enable race detector (requires CGO)
CGO_ENABLED=1 go test -race ./...

# With Ginkgo
CGO_ENABLED=1 ginkgo -race

# With coverage and race detection
CGO_ENABLED=1 go test -race -cover -covermode=atomic ./...

What it validates:

  • Mail object cloning safety
  • Concurrent sender creation
  • SMTP server state management
  • No data races in test helpers

Expected Output:

# ✅ Success
Ran 252 of 252 Specs in 1.553 seconds
SUCCESS! -- 252 Passed | 0 Failed | 0 Pending | 0 Skipped
PASS
ok  	github.com/nabbar/golib/mail/sender	2.738s

# ❌ Race detected (example - should never occur)
WARNING: DATA RACE
Read at 0x... by goroutine ...

Status: Zero data races detected

Performance & Profiling

# View benchmark results during tests
go test -v ./...

# Memory profiling
go test -memprofile=mem.out ./...
go tool pprof mem.out

# CPU profiling
go test -cpuprofile=cpu.out ./...
go tool pprof cpu.out

# Block profiling (goroutine blocking)
go test -blockprofile=block.out ./...
go tool pprof block.out

Performance Expectations

Test Type Duration Notes
Full Suite ~0.8s Without race
With -race ~1.5s 2x slower (expected)
Individual Spec <50ms Most tests
SMTP Tests 50-200ms Server startup overhead
Benchmarks Included gmeasure reports

Test Coverage

Target: ≥80% statement coverage
Current: 81.4%

Coverage By Category

Category Files Specs Description
Mail Operations mail_test.go 54 Create, clone, properties, headers, body, attachments
Email Addresses email_test.go 37 From, sender, replyTo, recipients, fallback behavior
Sending send_test.go 25 Sender creation, SMTP integration, context handling
Configuration config_test.go 43 Validation, mailer creation, file attachments
Types types_test.go 29 Encoding, priority, content type, recipient type
Errors errors_test.go 32 Error codes, wrapping, validation, edge cases
Edge Cases edge_cases_test.go 20 Empty values, large data, special chars, limits
Benchmarks benchmark_test.go 12 Performance measurements with gmeasure

View Coverage

# Generate coverage report
go test -coverprofile=coverage.out -covermode=atomic ./...

# View in terminal
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
start coverage.html  # Windows

Coverage Analysis

Well-Covered Areas (>85%)

  • Mail interface implementation
  • Email address management
  • Configuration validation
  • Type parsing and conversion
  • Error handling

Areas for Improvement (<75%)

  • Some edge cases in sender.go
  • Complex error scenarios
  • Date/time parsing edge cases

Test Structure

Tests follow Ginkgo's hierarchical BDD structure:

var _ = Describe("mail/sender Component", func() {
    var (
        ctx context.Context
        cnl context.CancelFunc
    )
    
    BeforeEach(func() {
        ctx, cnl = context.WithTimeout(context.Background(), 30*time.Second)
    })
    
    AfterEach(func() {
        if cnl != nil {
            cnl()
        }
    })
    
    Context("Feature or scenario", func() {
        It("should do something specific", func() {
            mail := sender.New()
            mail.SetSubject("Test")
            
            Expect(mail.GetSubject()).To(Equal("Test"))
        })
    })
})

Thread Safety

The mail/sender package has specific thread-safety characteristics that are validated through testing.

Thread-Safety Model

NOT Thread-Safe (by design):

  • Mail objects - Clone for concurrent use
  • Email interface - Part of Mail
  • Sender objects - One-time use

Thread-Safe:

  • Config objects - Immutable after creation
  • Type parsing functions - Pure functions
  • Error codes - Constants

Concurrent Usage Pattern

// ✅ Correct: Clone for concurrency
template := sender.New()
template.SetSubject("Notification")

for i := 0; i < 10; i++ {
    go func(index int) {
        mail := template.Clone() // Independent copy
        mail.Email().AddRecipients(sender.RecipientTo, fmt.Sprintf("user%d@example.com", index))
        // Use mail...
    }(i)
}

// ❌ Incorrect: Shared Mail object
mail := sender.New()
for i := 0; i < 10; i++ {
    go func() {
        mail.SetSubject("Test") // DATA RACE!
    }()
}

Testing Commands

# Full suite with race detection
CGO_ENABLED=1 go test -race -v ./...

# Focus on concurrent scenarios
CGO_ENABLED=1 go test -race -v -run "Clone\|Concurrent" ./...

# Stress test (10 iterations)
for i in {1..10}; do CGO_ENABLED=1 go test -race ./... || break; done

Result: Zero data races across all test runs


Test File Organization

File Purpose Specs Notes
sender_suite_test.go Suite initialization, test helpers 1 SMTP server setup
mail_test.go Mail interface operations 54 Core functionality
email_test.go Email address management 37 Recipients, addresses
send_test.go Sending operations 25 Real SMTP integration
config_test.go Configuration operations 43 Validation, creation
types_test.go Type definitions 29 Enums, parsing
errors_test.go Error handling 32 Error codes, wrapping
edge_cases_test.go Edge cases and limits 20 Boundary conditions
benchmark_test.go Performance benchmarks 12 gmeasure stats

Test Helper Infrastructure

sender_suite_test.go provides:

// Context management
var testCtx context.Context
var testCtxCancel context.CancelFunc

// SMTP test server backend
type testBackend struct {
    requireAuth bool
    messages    []testMessage
}

// SMTP session for testing
type testSession struct {
    backend *testBackend
    from    string
    to      []string
}

// Helper functions
func startTestSMTPServer(backend *testBackend, useTLS bool) (*smtpsv.Server, string, int, error)
func newTestConfig(host string, port int, tlsMode smtptp.TLSMode) *smtpcfg.Config
func newTestSMTPClient(host string, port int) libsmtp.SMTP
func newReadCloser(s string) io.ReadCloser

Writing Tests

Test File Template

package sender_test

import (
    "context"
    "time"
    
    libsnd "github.com/nabbar/golib/mail/sender"
    
    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
)

var _ = Describe("Component Name", func() {
    var (
        ctx  context.Context
        cnl  context.CancelFunc
        mail libsnd.Mail
    )
    
    BeforeEach(func() {
        ctx, cnl = context.WithTimeout(testCtx, 10*time.Second)
        mail = libsnd.New()
    })
    
    AfterEach(func() {
        if cnl != nil {
            cnl()
        }
    })
    
    Context("Scenario description", func() {
        It("should have expected behavior", func() {
            // Arrange
            mail.SetSubject("Test")
            
            // Act
            result := mail.GetSubject()
            
            // Assert
            Expect(result).To(Equal("Test"))
        })
    })
})

Common Gomega Matchers

// Equality
Expect(value).To(Equal(expected))
Expect(value).ToNot(Equal(unexpected))

// Nil checks
Expect(err).ToNot(HaveOccurred())
Expect(ptr).To(BeNil())

// Strings
Expect(str).To(ContainSubstring("substring"))
Expect(str).To(HavePrefix("prefix"))
Expect(str).To(MatchRegexp("pattern"))

// Collections
Expect(slice).To(HaveLen(5))
Expect(slice).To(ContainElement("item"))
Expect(slice).To(BeEmpty())

// Numeric
Expect(num).To(BeNumerically(">", 0))
Expect(num).To(BeNumerically("~", 1.5, 0.1))

// Boolean
Expect(condition).To(BeTrue())
Expect(condition).To(BeFalse())

// Types
Expect(obj).To(BeAssignableToTypeOf(&Type{}))

Benchmark Tests with gmeasure

It("should measure performance", func() {
    experiment := gmeasure.NewExperiment("Operation Name")
    AddReportEntry(experiment.Name, experiment)
    
    experiment.Sample(func(idx int) {
        experiment.MeasureDuration("operation", func() {
            // Code to benchmark
            mail := sender.New()
            mail.SetSubject("Test")
        })
    }, gmeasure.SamplingConfig{N: 1000})
    
    stats := experiment.GetStats("operation")
    AddReportEntry(experiment.Name+" Stats", 
        fmt.Sprintf("Mean: %v, StdDev: %v", stats.DurationFor(gmeasure.StatMean), 
        stats.DurationFor(gmeasure.StatStdDev)))
})

SMTP Integration Tests

It("should send email via SMTP", func() {
    // Setup SMTP test server
    backend := &testBackend{requireAuth: false, messages: make([]testMessage, 0)}
    smtpServer, host, port, err := startTestSMTPServer(backend, false)
    Expect(err).ToNot(HaveOccurred())
    defer smtpServer.Close()
    
    // Create SMTP client
    smtpClient := newTestSMTPClient(host, port)
    defer smtpClient.Close()
    
    // Create and configure email
    mail := sender.New()
    mail.SetSubject("Test")
    mail.Email().SetFrom("sender@test.com")
    mail.Email().AddRecipients(sender.RecipientTo, "recipient@test.com")
    body := newReadCloser("Test body")
    mail.SetBody(sender.ContentPlainText, body)
    
    // Send
    snd, err := mail.Sender()
    Expect(err).ToNot(HaveOccurred())
    defer snd.Close()
    
    err = snd.Send(ctx, smtpClient)
    Expect(err).ToNot(HaveOccurred())
    
    // Verify
    Expect(backend.messages).To(HaveLen(1))
    Expect(backend.messages[0].from).To(Equal("sender@test.com"))
})

Best Practices

Organize Tests Logically

// ✅ Good: Clear hierarchy
Describe("Mail Interface", func() {
    Context("Subject Operations", func() {
        It("should set subject", func() { /* ... */ })
        It("should get subject", func() { /* ... */ })
    })
    
    Context("Body Operations", func() {
        It("should set plain text body", func() { /* ... */ })
        It("should add HTML body", func() { /* ... */ })
    })
})

// ❌ Bad: Flat structure
Describe("Tests", func() {
    It("test 1", func() { /* ... */ })
    It("test 2", func() { /* ... */ })
    It("test 3", func() { /* ... */ })
})

Use Descriptive Names

// ✅ Good: Clear intent
It("should return error when from address is invalid", func() { /* ... */ })

// ❌ Bad: Vague
It("should fail", func() { /* ... */ })

Clean Up Resources

// ✅ Good: Proper cleanup
It("should send with attachment", func() {
    file, _ := os.Open("test.pdf")
    // Don't defer here - let sender close it
    mail.AddAttachment("test.pdf", "application/pdf", file, false)
    
    snd, _ := mail.Sender()
    defer snd.Close() // Closes file too
})

// ❌ Bad: Resource leak
It("should send", func() {
    file, _ := os.Open("test.pdf")
    mail.AddAttachment("test.pdf", "application/pdf", file, false)
    // File never closed if test fails
})

Test Edge Cases

Context("Edge Cases", func() {
    It("should handle empty subject", func() {
        mail.SetSubject("")
        Expect(mail.GetSubject()).To(Equal(""))
    })
    
    It("should handle very long subject", func() {
        longSubject := strings.Repeat("A", 10000)
        mail.SetSubject(longSubject)
        Expect(mail.GetSubject()).To(Equal(longSubject))
    })
    
    It("should handle special characters", func() {
        mail.SetSubject("Test 日本語 émojis 🎉")
        Expect(mail.GetSubject()).To(ContainSubstring("🎉"))
    })
})

Use Table-Driven Tests for Patterns

DescribeTable("Encoding parsing",
    func(input string, expected sender.Encoding) {
        result := sender.ParseEncoding(input)
        Expect(result).To(Equal(expected))
    },
    Entry("Base64", "Base 64", sender.EncodingBase64),
    Entry("Quoted-Printable", "Quoted Printable", sender.EncodingQuotedPrintable),
    Entry("Case insensitive", "base 64", sender.EncodingBase64),
    Entry("Invalid defaults to None", "invalid", sender.EncodingNone),
)

Troubleshooting

Common Issues

1. Race Detector Not Working

# Problem: Race detector requires CGO
go test -race ./...
# Error: -race requires cgo

# Solution: Enable CGO
CGO_ENABLED=1 go test -race ./...

2. Tests Timeout

# Problem: Default 10m timeout exceeded
go test ./...
# FAIL: timeout after 10m0s

# Solution: Increase timeout
go test -timeout=30m ./...

3. SMTP Server Port Conflicts

# Problem: Port already in use
# Error: bind: address already in use

# Solution 1: Kill conflicting process
lsof -i :25 | grep LISTEN | awk '{print $2}' | xargs kill

# Solution 2: Tests use random free ports (already implemented)
# The test suite automatically finds free ports

4. Coverage Report Not Generated

# Problem: Missing coverage file
go tool cover -html=coverage.out
# Error: coverage.out: no such file

# Solution: Generate coverage first
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

5. Ginkgo Not Found

# Problem: Ginkgo CLI not installed
ginkgo
# Error: command not found

# Solution: Install Ginkgo
go install github.com/onsi/ginkgo/v2/ginkgo@latest

# Verify installation
ginkgo version

Debug Failing Tests

# Run specific test
go test -v -run "TestSender/Mail_Operations/Subject"

# With Ginkgo focus
ginkgo --focus="should set subject"

# Print debug output (add to test)
It("should work", func() {
    GinkgoWriter.Println("Debug:", value)
    // Test code...
})

# Fail fast on first error
go test -failfast ./...

Performance Issues

# Identify slow tests
go test -v ./... | grep "seconds"

# Profile CPU usage
go test -cpuprofile=cpu.out ./...
go tool pprof cpu.out
# In pprof: top10, list <function>

# Profile memory
go test -memprofile=mem.out ./...
go tool pprof mem.out
# In pprof: top10, list <function>

CI Integration

GitHub Actions Example

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go-version: ['1.18', '1.19', '1.20', '1.21']
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Go
        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 -cover ./...
      
      - name: Race detection
        run: CGO_ENABLED=1 go test -race -v ./...
      
      - name: Generate coverage
        run: go test -coverprofile=coverage.out -covermode=atomic ./...
      
      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage.out
          flags: unittests

GitLab CI Example

test:
  image: golang:1.21
  stage: test
  script:
    - go mod download
    - go test -v -cover ./...
    - CGO_ENABLED=1 go test -race ./...
  coverage: '/coverage: \d+\.\d+% of statements/'
  artifacts:
    reports:
      junit: report.xml
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

Jenkins Pipeline Example

pipeline {
    agent any
    
    stages {
        stage('Test') {
            steps {
                sh 'go mod download'
                sh 'go test -v -cover ./...'
            }
        }
        
        stage('Race Detection') {
            steps {
                sh 'CGO_ENABLED=1 go test -race ./...'
            }
        }
        
        stage('Coverage') {
            steps {
                sh 'go test -coverprofile=coverage.out ./...'
                sh 'go tool cover -html=coverage.out -o coverage.html'
                publishHTML([reportDir: '.', reportFiles: 'coverage.html', reportName: 'Coverage Report'])
            }
        }
    }
}

Pre-Commit Hook

#!/bin/bash
# .git/hooks/pre-commit

echo "Running tests..."
go test ./...
if [ $? -ne 0 ]; then
    echo "Tests failed. Commit aborted."
    exit 1
fi

echo "Running race detector..."
CGO_ENABLED=1 go test -race ./...
if [ $? -ne 0 ]; then
    echo "Race detector found issues. Commit aborted."
    exit 1
fi

echo "All checks passed!"
exit 0

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