Overview
Camera Workflow uses Go’s built-in testing framework with comprehensive test coverage for critical components.
Running Tests
Basic Test Run
Run all tests with verbose output:
This executes:
Test with Coverage
Generate coverage reports:
Generates:
coverage.out - Machine-readable coverage data
coverage.html - HTML coverage report (open in browser)
Features:
- Race condition detection (
-race flag)
- Line-by-line coverage visualization
- Package-level coverage statistics
View Coverage Report
After running make test-coverage:
open coverage.html # macOS
xdg-open coverage.html # Linux
Test Organization
Tests are located alongside the code they test:
internal/
├── checksum/
│ ├── checksum.go
│ └── checksum_test.go
├── converter/
│ ├── adaptive_limiter.go
│ ├── adaptive_limiter_test.go
│ ├── adaptive_workers.go
│ ├── adaptive_workers_test.go
│ ├── copy_mode.go
│ └── copy_mode_test.go
└── ...
Test Coverage by Package
Checksum Package
File: internal/checksum/checksum_test.go
Tests:
- xxHash64 calculation accuracy
- File vs. reader-based hashing
- Checksum formatting
- Error handling
go test -v ./internal/checksum/
Converter Package
Files:
internal/converter/adaptive_limiter_test.go
internal/converter/adaptive_workers_test.go
internal/converter/copy_mode_test.go
Tests:
- Adaptive worker scaling logic
- Resource-based worker adjustment
- Copy-only mode operations
- Semaphore behavior
go test -v ./internal/converter/
Writing Tests
Test File Naming
Follow Go conventions:
- Test files end with
_test.go
- Test functions start with
Test
- Example:
checksum_test.go for checksum.go
Example Test Structure
package checksum_test
import (
"testing"
"github.com/Azilone/Camera-Workflow/internal/checksum"
)
func TestXXHash64Calculation(t *testing.T) {
hasher := checksum.NewXXHash64Hasher()
// Test with known input
sum, err := hasher.CalculateReader(strings.NewReader("test data"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if sum == 0 {
t.Error("expected non-zero checksum")
}
}
Table-Driven Tests
Use table-driven tests for multiple scenarios:
func TestGetMonthName(t *testing.T) {
tests := []struct {
month int
language string
want string
}{
{1, "en", "01-January"},
{1, "fr", "01-Janvier"},
{12, "es", "12-Diciembre"},
{6, "de", "06-Juni"},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%d_%s", tt.month, tt.language), func(t *testing.T) {
got := utils.GetMonthName(tt.month, tt.language)
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
Testing Best Practices
Isolate External Dependencies
Avoid tests that depend on:
- Network connections
- File system state
- External services (FFmpeg, ImageMagick)
Use mocks or stubs for external tools:type MockHasher struct {
CalculateFunc func(path string) (uint64, error)
}
func (m *MockHasher) Calculate(path string) (uint64, error) {
if m.CalculateFunc != nil {
return m.CalculateFunc(path)
}
return 0, nil
}
Always test error conditions:func TestCalculateWithInvalidFile(t *testing.T) {
hasher := checksum.NewXXHash64Hasher()
_, err := hasher.Calculate("/nonexistent/file")
if err == nil {
t.Error("expected error for nonexistent file")
}
}
Create helper functions for common setup:func createTempFile(t *testing.T, content string) string {
t.Helper()
f, err := os.CreateTemp("", "test-*")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
defer f.Close()
if _, err := f.WriteString(content); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}
t.Cleanup(func() { os.Remove(f.Name()) })
return f.Name()
}
Use t.Cleanup() for resource cleanup:func TestWithTempDir(t *testing.T) {
dir, err := os.MkdirTemp("", "test-*")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
// Test code using dir
}
Integration Tests
For tests requiring FFmpeg/ImageMagick:
Skip if Dependencies Missing
func TestImageConversion(t *testing.T) {
if err := utils.CheckDependencies(); err != nil {
t.Skip("skipping test: missing dependencies")
}
// Test image conversion
}
Separate integration tests with build tags:
//go:build integration
package converter_test
import "testing"
func TestFullConversionPipeline(t *testing.T) {
// Integration test requiring external tools
}
Run integration tests:
go test -tags=integration ./...
Benchmarking
Write Benchmarks
func BenchmarkXXHash64(b *testing.B) {
data := make([]byte, 1024*1024) // 1MB
hasher := checksum.NewXXHash64Hasher()
b.ResetTimer()
for i := 0; i < b.N; i++ {
hasher.CalculateReader(bytes.NewReader(data))
}
}
Run Benchmarks
go test -bench=. -benchmem ./internal/checksum/
Output:
BenchmarkXXHash64-8 1000 1234567 ns/op 123456 B/op 12 allocs/op
Continuous Integration
The project uses GitHub Actions for CI:
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg imagemagick
- name: Run tests
run: make test-coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.out
Test Coverage Goals
Critical Paths
Target: 80%+
- Security checks
- File integrity verification
- Configuration validation
Business Logic
Target: 70%+
- Conversion orchestration
- Worker management
- Progress tracking
Utilities
Target: 60%+
- File handling
- Date extraction
- Format detection
Integration
Manual Testing
- FFmpeg integration
- ImageMagick integration
- Platform-specific code
Race Detector
Detect race conditions in concurrent code:
Race detector has significant overhead. Only use during testing, not production.
Test Coverage by Function
View function-level coverage:
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
Output:
github.com/Azilone/Camera-Workflow/internal/checksum/checksum.go:26: Calculate 100.0%
github.com/Azilone/Camera-Workflow/internal/checksum/checksum.go:37: CalculateReader 100.0%
github.com/Azilone/Camera-Workflow/internal/checksum/checksum.go:48: FormatChecksum 100.0%
Verbose Test Output
See detailed test execution:
go test -v -run TestSpecificFunction ./internal/checksum/
Test Examples
Testing Adaptive Workers
func TestAdaptiveWorkerScaling(t *testing.T) {
limiter := NewAdaptiveLimiter(2)
// Start with 2 workers
if limiter.CurrentLimit() != 2 {
t.Errorf("expected 2, got %d", limiter.CurrentLimit())
}
// Scale up to 4
limiter.SetLimit(4)
if limiter.CurrentLimit() != 4 {
t.Errorf("expected 4, got %d", limiter.CurrentLimit())
}
// Scale down to 1
limiter.SetLimit(1)
if limiter.CurrentLimit() != 1 {
t.Errorf("expected 1, got %d", limiter.CurrentLimit())
}
}
Testing Checksum Calculation
func TestChecksumConsistency(t *testing.T) {
hasher := checksum.NewXXHash64Hasher()
data := []byte("consistent test data")
// Calculate multiple times
sum1, _ := hasher.CalculateReader(bytes.NewReader(data))
sum2, _ := hasher.CalculateReader(bytes.NewReader(data))
if sum1 != sum2 {
t.Error("checksum not consistent")
}
}
Debugging Tests
Print Debug Output
func TestWithDebug(t *testing.T) {
t.Logf("Debug: processing file %s", filename)
result := processFile(filename)
t.Logf("Debug: result = %+v", result)
if result.Error != nil {
t.Errorf("unexpected error: %v", result.Error)
}
}
View debug output:
Run Specific Tests
# Run specific test
go test -run TestXXHash64 ./internal/checksum/
# Run tests matching pattern
go test -run TestAdaptive.* ./internal/converter/
Next Steps
Building
Learn how to build the project
Architecture
Understand the system design
Contributing
Contribute to the project