cache-apt-pkgs-action/CLAUDE.md
2025-10-04 23:38:32 -07:00

38 KiB

Code Improvements by Claude

Table of Contents

General Code Organization Principles

1. Package Structure

  • Keep packages focused and single-purpose
  • Use internal packages for code not meant to be imported
  • Organize by feature/domain rather than by type
  • Follow Go standard layout conventions

2. Code Style and Formatting

  • Use 2 spaces for indentation, never tabs
  • Consistent naming conventions (e.g., CamelCase for exported names)
  • Keep functions small and focused
  • Use meaningful variable names
  • Follow standard Go formatting guidelines
  • Use comments to explain "why" not "what"

3. Error Handling

  • Return errors rather than using panics
  • Wrap errors with context when crossing package boundaries
  • Create custom error types only when needed for client handling
  • Use sentinel errors sparingly

4. API Design

5. Documentation Practices

Go Code Documentation Standards

Following the official Go Documentation Guidelines:

  1. Package Documentation

    • Every package must have a doc comment immediately before the package statement
    • Format: // Package xyz ... (first sentence) followed by detailed description
    • First sentence should be a summary beginning with Package xyz
    • Follow with a blank line and detailed documentation
    • Include package-level examples if helpful
  2. Exported Items Documentation

    • Document all exported (capitalized) names
    • Comments must begin with the name being declared
    • First sentence should be a summary
    • Omit the subject when it's the thing being documented
    • Use article "a" for types that could be one of many, "the" for singletons

    Examples:

    // List represents a singly-linked list.
    // A zero List is valid and represents an empty list.
    type List struct {}
    
    // NewRing creates a new ring buffer with the given size.
    func NewRing(size int) *Ring {}
    
    // Append adds the elements to the list.
    // Blocks if buffer is full.
    func (l *List) Append(elems ...interface{}) {}
    
  3. Documentation Style

    • Write clear, complete sentences
    • Begin comments with a capital letter
    • End sentences with punctuation
    • Keep comments up to date with code changes
    • Focus on behavior users can rely on, not implementation
    • Document synchronization assumptions for concurrent access
    • Document any special error conditions or panics
  4. Examples

    • Add examples for complex types or functions using Example functions

    • Include examples in package docs for important usage patterns

    • Make examples self-contained and runnable

    • Use realistic data and common use cases

    • Show output in comments when examples print output:

      func ExampleHello() {
          fmt.Println("Hello")
          // Output: Hello
      }
      
  5. Doc Comments Format

    • Use complete sentences and proper punctuation
    • Add a blank line between paragraphs
    • Use lists and code snippets for clarity
    • Include links to related functions/types where helpful
    • Document parameters and return values implicitly in the description
    • Break long lines at 80 characters
  6. Quality Control

    • Run go doc to verify how documentation will appear
    • Review documentation during code reviews
    • Keep examples up to date and passing
    • Update docs when changing behavior

Code Documentation

  • Write package documentation with examples
  • Document exported symbols comprehensively
  • Include usage examples in doc comments
  • Document concurrency safety
  • Add links to related functions/types

Example:

// key.go
//
// Description:
//
// Provides types and functions for managing cache keys, including serialization, deserialization,
//  and validation of package metadata.
//
// Package: cache
//
// Example usage:
//
// // Create a new cache key
// key := cache.NewKey(packages, "v1.0", "v2", "amd64")
//
// // Get the hash of the key
// hash := key.Hash()
// fmt.Printf("Key hash: %x\n", hash)
package cache

Project Documentation

  • Maintain a comprehensive README
  • Include getting started guide
  • Document all configuration options
  • Add troubleshooting guides
  • Keep changelog updated
  • Include contribution guidelines

6. Testing Strategy

Types of Tests

  1. Unit Tests

    • Test individual components
    • Mock dependencies
    • Focus on behavior not implementation
  2. Integration Tests

    • Test component interactions
    • Use real dependencies
    • Test complete workflows
  3. End-to-End Tests

    • Test full system
    • Use real external services
    • Verify key user scenarios

Test Coverage Strategy

  • Aim for high but meaningful coverage
  • Focus on critical paths
  • Test edge cases and error conditions
  • Balance cost vs benefit of testing
  • Document untested scenarios

7. Security Best Practices

Input Validation

  • Validate all external input
  • Use strong types over strings
  • Implement proper input validation and cleaning
  • Assert array bounds
  • Validate file paths

Secure Coding

  • Use latest dependencies
  • Implement proper error handling
  • Avoid command injection
  • Use secure random numbers
  • Follow principle of least privilege

Secrets Management

  • Never commit secrets
  • Use environment variables
  • Implement secure configuration loading
  • Rotate credentials regularly
  • Log access to sensitive operations

8. Performance Considerations

  • Minimize allocations in hot paths
  • Use sync.Pool for frequently allocated objects
  • Consider memory usage in data structures
  • Profile before optimizing
  • Document performance characteristics

9. Profiling and Benchmarking

CPU Profiling

import "runtime/pprof"

func main() {
    // Create CPU profile
    f, _ := os.Create("cpu.prof")
    defer f.Close()
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()

    // Your code here
}

View with:

go tool pprof cpu.prof

Memory Profiling

import "runtime/pprof"

func main() {
    // Create memory profile
    f, _ := os.Create("mem.prof")
    defer f.Close()
    // Run your code
    pprof.WriteHeapProfile(f)
}

View with:

go tool pprof -alloc_objects mem.prof

Benchmarking

Create benchmark tests with naming pattern Benchmark<Function>:

func BenchmarkMyFunction(b *testing.B) {
    for i := 0; i < b.N; i++ {
        MyFunction()
    }
}

Run with:

go test -bench=. -benchmem

Trace Profiling

import "runtime/trace"

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()
    trace.Start(f)
    defer trace.Stop()

    // Your code here
}

View with:

go tool trace trace.out

Common Profiling Tasks

  1. CPU Usage

    # Profile for 30 seconds
    go test -cpuprofile=cpu.prof -bench=.
    go tool pprof cpu.prof
    
  2. Memory Allocations

    # Track allocations
    go test -memprofile=mem.prof -bench=.
    go tool pprof -alloc_objects mem.prof
    
  3. Goroutine Block Profiling

    # Track goroutine blocks
    go test -blockprofile=block.prof -bench=.
    go tool pprof block.prof
    
  4. Mutex Contention

    # Track mutex contention
    go test -mutexprofile=mutex.prof -bench=.
    go tool pprof mutex.prof
    

pprof Web Interface

For visual analysis:

go tool pprof -http=:8080 cpu.prof

Key Metrics to Watch

  1. CPU Profile

    • Hot functions
    • Call graph
    • Time per call
    • Call count
  2. Memory Profile

    • Allocation count
    • Allocation size
    • Temporary allocations
    • Leak suspects
  3. Goroutine Profile

    • Active goroutines
    • Blocked goroutines
    • Scheduling latency
    • Stack traces
  4. Trace Analysis

    • GC frequency
    • GC duration
    • Goroutine scheduling
    • Network/syscall blocking

10. Concurrency Patterns

  • Use channels for coordination, mutexes for state
  • Keep critical sections small
  • Document concurrency safety
  • Use context for cancellation
  • Consider rate limiting and load shedding

11. Configuration Management

  • Use environment variables for deployment-specific values
  • Validate configuration at startup
  • Provide sensible defaults
  • Support multiple configuration sources
  • Document all configuration options

12. Logging and Observability

  • Use structured logging
  • Include relevant context in logs
  • Define log levels appropriately
  • Add tracing for complex operations
  • Include metrics for important operations

Non-Go Files

GitHub Actions

Action File Formatting

  • Minimize the amount of shell code and put complex logic in the Go code
  • Use clear step id names that use dashes between words and active verbs
  • Avoid hard-coded API URLs like https://api.github.com. Use environment variables (GITHUB_API_URL for REST API, GITHUB_GRAPHQL_URL for GraphQL) or the @actions/github toolkit for dynamic URL handling
Release Management
  • Use semantic versioning for releases (e.g., v1.0.0)
  • Recommend users reference major version tags (v1) instead of the default branch for stability.
  • Update major version tags to point to the latest release
Create a README File

Include a detailed description, required/optional inputs and outputs, secrets, environment variables, and usage examples

Testing and Automation
  • Add workflows to test your action on feature branches and pull requests
  • Automate releases using workflows triggered by publishing or editing a release.
Community Engagement
  • Maintain a clear README with examples.
  • Add community health files like CODE_OF_CONDUCT and CONTRIBUTING.
  • Use badges to display workflow status.
Further Guidance

For more details, visit:

YAML Formatting

Quoting Guidelines

Follow these rules for consistent YAML formatting:

DO quote when required:

# Strings with special characters or spaces
version: "test version with spaces"
name: "app-v1.2.3"
message: "Value contains: colons, commas, quotes"

# Empty strings
packages: ""
input: ""

# Values that could be interpreted as other types
id: "123" # Prevents interpretation as number
flag: "true" # Prevents interpretation as boolean
version: "1.0" # Prevents interpretation as number

# YAML special values that should be strings
value: "null" # String "null", not null value
enable: "false" # String "false", not boolean false

DO NOT quote simple values:

# Booleans
debug: false
enabled: true

# Numbers
count: 42
version: 1.2

# Simple strings without special characters
name: ubuntu-latest
step: checkout
action: setup-node

# GitHub Actions expressions (never quote these)
if: github.event_name == 'push'
with: ${{ secrets.TOKEN }}

GitHub Actions specific guidelines:

# Action references - never quote
uses: actions/checkout@v4
uses: ./path/to/local/action

# Boolean inputs - don't quote
debug: false
cache: true

# Version strings with special chars - quote if needed
version: "v1.2.3-beta"

# Expressions - never quote
if: ${{ github.ref == 'refs/heads/main' }}
run: echo "${{ github.actor }}"

Formatting Standards

  • Use 2 spaces for indentation
  • Use - for list items with proper indentation
  • Keep consistent spacing around colons
  • Use block scalar | for multiline strings
  • Use folded scalar > for wrapped text

Example of well-formatted YAML:

name: CI Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: |
          npm install
          npm test          
        env:
          NODE_ENV: test
          DEBUG: false

Bash Scripts

Project scripts should follow these guidelines:

  • Follow formatting rules in ShellCheck
  • Follow style guide rules in Google Bash Style Guide
  • Include proper error handling and exit codes
  • Use scripts/lib.sh whenever for common functionality
  • Use imperative verb form for script names:
    • Good: export_version.sh, build_package.sh, run_tests.sh
    • Bad: version_export.sh, package_builder.sh, test_runner.sh
  • Create scripts in the scripts directory (not tools)
  • Make scripts executable (chmod +x)
  • Add new functionality to the scripts/menu.sh script for easy access
  • Add usage information (viewable with -h or --help)

Script Header Format:

#==============================================================================
# script_name.sh
#==============================================================================
#
# DESCRIPTION:
#   Brief description of what the script does.
#   Additional details if needed.
#
# USAGE:
#   ./scripts/script_name.sh [options]
#
# OPTIONS: (if applicable)
#   List of command-line options and their descriptions
#
# DEPENDENCIES:
#   - List of required tools and commands
#==============================================================================

Every script should include this header format at the top, with all sections filled out appropriately. The header provides:

  • Clear identification of the script
  • Description of its purpose and functionality
  • Usage instructions and examples
  • Documentation of command-line options (if any)
  • List of required dependencies

Script Testing

All scripts must have corresponding tests in the scripts/tests directory using the common test library:

  1. Test File Structure

    • Name test files as <script_name>_test.sh
    • Place in scripts/tests directory
    • Make test files executable (chmod +x)
    • Source the common test library (test_lib.sh)
  2. Common Test Library The test_lib.sh library provides a standard test framework. See the scripts/tests/template_test.sh for examples of how to set up one.

  3. Test Organization

    • Group related test cases into sections
    • Test each command/flag combination
    • Test error conditions explicitly
  4. Test Coverage

    • Test error conditions
    • Test input validation
    • Test edge cases
    • Test each supported flag/option
  5. CI Integration

    • Tests run automatically in CI
    • Tests must pass before merge
    • Test execution is part of the validate-scripts job
    • Test failures block PR merges
Test Framework Architecture Pattern

The improved test framework follows this standardized pattern for all script tests:

Test File Template:

#!/bin/bash

#==============================================================================
# script_name_test.sh
#==============================================================================
#
# DESCRIPTION:
#   Test suite for script_name.sh functionality.
#   Brief description of what aspects are tested.
#
# USAGE:
#   script_name_test.sh [OPTIONS]
#
# OPTIONS:
#   -v, --verbose        Enable verbose test output
#   --stop-on-failure    Stop on first test failure
#   -h, --help           Show this help message
#
#==============================================================================

# Set up the script path we want to test
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
export SCRIPT_PATH="$SCRIPT_DIR/../script_name.sh"

# Source the test framework
source "$SCRIPT_DIR/test_lib.sh"

# Define test functions
run_tests() {
  test_section "Help and Usage"

  test_case "shows help message" \
    "--help" \
    "USAGE:" \
    true

  test_case "shows error for invalid option" \
    "--invalid-option" \
    "Unknown option" \
    false

  test_section "Core Functionality"

  # Add more test cases here
}

# Start the test framework and run tests
start_tests "$@"
run_tests

Key Framework Features:

  • SCRIPT_PATH Setup: Test files must set SCRIPT_PATH before sourcing test_lib.sh to avoid variable conflicts
  • Function-based Test Organization: Tests are organized in a run_tests() function called after framework initialization
  • Consistent Test Sections: Use test_section to group related tests with descriptive headers
  • Standard Test Case Pattern: test_case "name" "args" "expected_output" "should_succeed"
  • Framework Integration: Call start_tests "$@" before running tests to handle argument parsing and setup
Script Argument Parsing Pattern

All scripts should implement consistent argument parsing following this pattern:

main() {
  # Parse command line arguments first
  while [[ $# -gt 0 ]]; do
    case $1 in
    -v | --verbose)
      export VERBOSE=true
      ;;
    -h | --help)
      cat << 'EOF'
USAGE:
  script_name.sh [OPTIONS]

DESCRIPTION:
  Brief description of what the script does.
  Additional details if needed.

OPTIONS:
  -v, --verbose    Enable verbose output
  -h, --help       Show this help message
EOF
      exit 0
      ;;
    *)
      echo "Unknown option: $1" >&2
      echo "Use --help for usage information." >&2
      exit 1
      ;;
    esac
    shift
  done

  # Script main logic here
}

main "$@"

Key Argument Parsing Features:

  • Consistent Options: All scripts support -v/--verbose and -h/--help
  • Early Help Exit: Help is displayed immediately without running script logic
  • Error Handling: Unknown options produce helpful error messages
  • Inline Help Text: Help is embedded in the script using heredoc syntax
Centralized Configuration Management

The project implements centralized version management using the .env file as a single source of truth:

Configuration Structure:

# .env file contents
GO_VERSION=1.23.4
GO_TOOLCHAIN=go1.23.4

GitHub Actions Integration:

# .github/workflows/ci.yml pattern
jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      go-version: ${{ steps.env.outputs.go-version }}
    steps:
      - uses: actions/checkout@v4
      - id: env
        run: |
          source .env
          echo "go-version=$GO_VERSION" >> $GITHUB_OUTPUT          

  dependent-job:
    needs: setup
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ needs.setup.outputs.go-version }}

Synchronization Script Pattern:

  • scripts/sync_go_version.sh reads .env and updates go.mod accordingly
  • Ensures consistency between environment configuration and Go module requirements
  • Can be extended for other configuration synchronization needs
Implementation Status

Implemented Patterns:

The following scripts have been updated with the standardized patterns:

  1. scripts/export_version.sh - Complete implementation:

    • Argument parsing with --help and --verbose
    • Proper error handling and logging
    • Comprehensive test suite in scripts/tests/export_version_test.sh
    • Function-based test organization
  2. scripts/setup_dev.sh - Complete implementation:

    • Argument parsing with --help and --verbose
    • Script-specific help documentation
    • Error handling for unknown options
    • Comprehensive test suite in scripts/tests/setup_dev_test.sh
    • Function-based test organization
  3. scripts/tests/test_lib.sh - Framework improvements:

    • Reliable library loading with fallback paths
    • Safe SCRIPT_PATH variable handling
    • Arithmetic operations compatible with set -e
    • Proper script name detection
    • Lazy temporary directory initialization
    • Comprehensive documentation and architecture notes
  4. Centralized Configuration Management:

    • .env file as single source of truth for versions
    • GitHub Actions CI integration with version propagation
    • scripts/sync_go_version.sh for configuration synchronization

🔄 Remaining Scripts to Update:

These scripts need the same pattern implementations:

  • scripts/distribute.sh - Needs argument parsing and testing
  • scripts/update_md_tocs.sh - Needs argument parsing and testing
  • scripts/check_and_fix_env.sh - Needs argument parsing and testing
  • scripts/template.sh - Needs argument parsing and testing
  • scripts/menu.sh - Needs argument parsing and testing

Example script structure:

#!/bin/bash

#==============================================================================
# fix_and_update.sh
#==============================================================================
#
# DESCRIPTION:
#   Runs lint fixes and checks for UTF-8 formatting issues in the project.
#   Intended to help maintain code quality and formatting consistency.
#
# USAGE:
#   ./scripts/fix_and_update.sh
#
# OPTIONS:
#   -h, --help   Show this help message
#
# DEPENDENCIES:
#   - trunk (for linting)
#   - bash
#   - ./scripts/check_utf8.sh
#==============================================================================

# Resolves to absolute path and loads library
source "$(cd "$(dirname "$0")" && pwd)/lib.sh"

main_menu() {
  if false; then
    show_help   # Uses the script header to output usage message
  fi
}

# ...
# Script logic, variables and functions.
# ...

# Parse common command line arguments and hand the remaining to the script
remaining_args=$(parse_common_args "$@")

# Run main menu
main_menu

YAML Files

YAML files in the project (GitHub Actions workflows, configuration files, etc.) should follow these best practices:

Quoting Guidelines

  • Avoid unnecessary quotes - YAML values don't need quotes unless they contain special characters
  • Use quotes when required:
    • Values containing spaces: version: "test version with spaces"
    • Empty strings: packages: ""
    • Values starting with special characters: value: "@special"
    • Boolean-like strings that should be treated as strings: value: "true" (if you want the string "true", not boolean)
    • Numeric-like strings: version: "1.0" (if you want string "1.0", not number 1.0)

Examples

Good - No unnecessary quotes:

name: Test Action
on: workflow_dispatch
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./
        with:
          packages: curl wget
          version: test-1.0
          debug: true

Avoid - Unnecessary quotes:

name: "Test Action"
on: "workflow_dispatch"
jobs:
  test:
    runs-on: "ubuntu-latest"
    steps:
      - uses: "actions/checkout@v4"
      - uses: "./"
        with:
          packages: "curl wget"
          version: "test-1.0"
          debug: "true"

Good - Quotes when needed:

# Quotes required for values with spaces
version: "test version with spaces"

# Quotes required for empty strings
packages: ""

# No quotes needed for simple values
debug: true
timeout: 300
name: test-job

Formatting Guidelines

  • Use 2-space indentation consistently
  • Keep lines under 120 characters when possible
  • Use | for multi-line strings that need line breaks preserved
  • Use > for multi-line strings that should be folded
  • Align nested items consistently
  • Use meaningful names for job IDs and step IDs (use kebab-case)

Multi-line Strings

# For scripts that need line breaks preserved
run: |
  echo "Line 1"
  echo "Line 2"
  if [[ condition ]]; then
    echo "Line 3"
  fi  

# For long descriptions that should be folded
description: >
  This is a very long description that
  will be folded into a single line
  when parsed by YAML.  

# For package lists (GitHub Actions input)
packages: |
  curl
  wget
  jq  

GitHub Actions Specific

  • Use unquoted boolean values: required: true, debug: false
  • Use unquoted numeric values: timeout-minutes: 30
  • Quote version strings that might be interpreted as numbers: version: "1.0"
  • Use kebab-case for input/output names: cache-hit, package-version-list
  • Use meaningful step IDs: test-basic-install, verify-cache-hit

Testing Principles

1. Test Organization Strategy

We established a balanced approach to test organization:

  • Use table-driven tests for simple, repetitive cases without introducing logic
  • Use individual test functions for cases that require specific Arrange, Act, Assert steps that cannot be shared amongst other cases
  • Group related test cases that operate on the same API method / function

2. Code Structure

Constants and Variables

const (
    manifestVersion = "1.0.0"
    manifestGlobalVer = "v2"
)

var (
    fixedTime = time.Date(2025, 8, 28, 10, 0, 0, 0, time.UTC)
    sampleData = NewTestData()
)
  • Define constants for fixed values where the presence and format is only needed and the value content itself does not affect the behavior under test
  • Use variables for reusable test data
  • Group related constants and variables together
  • Do not prefix constants or variables with test

Helper Functions

Simple examples of factory and assert functions.

func createTestFile(t *testing.T, dir string, content string) string {
    t.Helper()
    // ... helper implementation
}

func assertValidJSON(t *testing.T, data string) {
    t.Helper()
    // ... assertion implementation
}

Example of using functions to abstract away details not relevant to the behavior under test

type Item struct {
  Name            string
  Description     string
  Version         string
  LastModified    Date
}

// BAD: Mixed concerns, unclear test name, magic values
func TestItem_Description(t *testing.T) {
    item := Item{
        Name:         "test item",
        Description:  "original description",
        Version:      "1.0.0",
        LastModified: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
    }

    AddPrefixToDescription(&item, "prefix: ")

    if item.Description != "prefix: original description" {
        t.Errorf("got %q, want %q", item.Description, "prefix: original description")
    }
}

// GOOD: Clear focus, reusable arrangement, proper assertions
const (
    defaultName       = "test item"
    defaultVersion    = "1.0.0"
    defaultTimeStr    = "2025-01-01T00:00:00Z"
)

func createTestItem(t *testing.T, description string) *Item {
    t.Helper()
    defaultTime, err := time.Parse(time.RFC3339, defaultTimeStr)
    if err != nil {
        t.Fatalf("failed to parse default time: %v", err)
    }

    return &Item{
        Name:         defaultName,
        Description:  description,
        Version:      defaultVersion,
        LastModified: defaultTime,
    }
}

func TestAddPrefixToDescription_WithValidInput_AddsPrefix(t *testing.T) {
    // Arrange
    item := createTestItem(t, "original description")
    const want := "prefix: original description"

    // Act
    AddPrefixToDescription(item, "prefix: ")

    // Assert
    assert.Equal(t, want, item.Description, "description should have prefix")
}
  • Create helper functions to reduce duplication and keeps tests focused on the arrangement inputs and how they correspond to the expected output
  • Use t.Helper() for proper test failure reporting
  • Keep helpers focused and single-purpose
  • Helper functions that require logic should go into their own file and have tests

3. Test Case Patterns

Table-Driven Tests (for simple cases)

// Each test case is its own function - no loops or conditionals in test body
func TestFormatMessage_WithEmptyString_ReturnsError(t *testing.T) {
    // Arrange
    input := ""

    // Act
    actual, err := FormatMessage(input)

    // Assert
    assertFormatError(t, actual, err, "input cannot be empty")
}

func TestFormatMessage_WithValidInput_ReturnsUpperCase(t *testing.T) {
    // Arrange
    input := "test message"
    expected := "TEST MESSAGE"

    // Act
    actual, err := FormatMessage(input)

    // Assert
    assertFormatSuccess(t, actual, err, expected)
}

func TestFormatMessage_WithMultipleSpaces_PreservesSpacing(t *testing.T) {
    // Arrange
    input := "hello  world"
    expected := "HELLO  WORLD"

    // Act
    actual, err := FormatMessage(input)

    // Assert
    assertFormatSuccess(t, actual, err, expected)
}

// Helper functions for common assertions
func assertFormatSuccess(t *testing.T, actual string, err error, expected string) {
    t.Helper()
    assert.NoError(t, err)
    assert.Equal(t, expected, actual, "formatted message should match")
}

func assertFormatError(t *testing.T, actual string, err error, expectedErrMsg string) {
    t.Helper()
    assert.Error(t, err)
    assert.Contains(t, err.Error(), expectedErrMsg)
    assert.Empty(t, actual)
}

Individual Tests (for complex cases)

func TestProcessTransaction_WithConcurrentUpdates_PreservesConsistency(t *testing.T) {
    // Arrange
    store := NewTestStore(t)
    defer store.Close()

    const accountID = "test-account"
    initialBalance := decimal.NewFromInt(1000)
    arrangeErr := arrangeTestAccount(t, store, accountID, initialBalance)
    require.NoError(t, arrangeErr)

    // Act
    actualBalance, err := executeConcurrentTransactions(t, store, accountID)

    // Assert
    expected := initialBalance.Add(decimal.NewFromInt(100)) // 100 transactions of 1 unit each
    assertBalanceEquals(t, expected, actualBalance)
}

// Helper functions to keep test body clean and linear
func arrangeTestAccount(t *testing.T, store *Store, accountID string, balance decimal.Decimal) error {
    t.Helper()
    return store.SetBalance(accountID, balance)
}

func executeConcurrentTransactions(t *testing.T, store *Store, accountID string) (decimal.Decimal, error) {
    t.Helper()
    const numTransactions = 100
    var wg sync.WaitGroup
    wg.Add(numTransactions)

    for i := 0; i < numTransactions; i++ {
        go func() {
            defer wg.Done()
            amount := decimal.NewFromInt(1)
            _, err := store.ProcessTransaction(accountID, amount)
            assert.NoError(t, err)
        }()
    }
    wg.Wait()

    return store.GetBalance(accountID)
}

func assertBalanceEquals(t *testing.T, expected, actual decimal.Decimal) {
    t.Helper()
    assert.True(t, expected.Equal(actual),
        "balance should be %s, actual was %s", expected, actual)
}

4. Best Practices Applied

  1. Clear Naming

    • Name test data clearly and meaningfully
    • Name by abstraction, not implementation
    • Use expected for expected values
    • Use actual for function results
    • Keep test variables consistent across all tests
    • Always use "Arrange", "Act", "Assert" as step comments in tests
    • Use descriptive test name arrangement and expectation parts
    • Use test name formats in a 3 part structure
      • Test<function>_<arrangement>_<expectation> for free functions, and
      • Test<interface><function>_<arrangement>_<expectation> for interface functions.
      • The module name is inferred
      • Treat the first part as either the type function or the free function under test
    func Test<[type]<function>>_<arrangement>_<expectation>(t *testing.T) {
      // Test body
    }
    
    // Implementation
    
    type Logger {
      debug bool
    }
    
    var logger Logger
    logger.debug = false
    
    func (l* Logger) Log(msg string) {
      // ...
    }
    
    func SetDebug(v bool) {
      logger.debug = v
    }
    
    // Test
    
    func TestLoggerLog_EmptyMessage_NothingLogged(t *testing.T) {
      // Test body
    }
    
    func TestSetDebug_PassFalseValue_DebugMessageNotLogged(t *testing.T) {
      // Test body
    }
    
  2. Test Structure

    • Keep test body simple and linear
    • No loops or conditionals in test body
    • Move complex arrangement to helper functions
    • Use table tests for multiple cases, not loops in test body
    • Extract complex assertions into helper functions
  3. Code Organization

    • Group related constants and variables
    • Place helper functions at the bottom
    • Organize tests by function under test
    • Follow arrange-act-assert pattern
  4. Test Data Management

    • Centralize test data definitions
    • Use constants for fixed values
    • Abstract complex data arrangement into helpers
  5. Error Handling

    • Test both success and error cases
    • Use clear error messages
    • Validate error types and messages
    • Handle expected and unexpected errors
  6. Assertions

    • Use consistent assertion patterns
    • Include helpful failure messages
    • Group related assertions logically
    • Test one concept per assertion

5. Examples of Improvements

Before

func TestFeature_MixedArrangements_ExpectAlotOfDifferentThings(t *testing.T) {
    // Mixed arrangement and assertions
    // Duplicated code
    // Magic values
}

After

// Before: Mixed concerns, unclear naming, magic values
func TestValidateConfig_MissingFileAndEmptyPaths_ValidationFails(t *testing.T) {
    c := &Config{
        Path: "./testdata",
        Port: 8080,
        MaxRetries: 3,
    }

    if err := c.Validate(); err != nil {
        t.Error("validation failed")
    }

    c.Path = ""
    if err := c.Validate(); err == nil {
        t.Error("expected error for empty path")
    }
}

// After: Clear structure, meaningful constants, proper test naming
const (
    testConfigPath    = "./testdata"
    defaultPort       = 8080
    defaultMaxRetries = 3
)

func TestValidateConfig_WithValidInputs_Succeeds(t *testing.T) {
    // Arrange
    config := &Config{
        Path:       testConfigPath,
        Port:       defaultPort,
        MaxRetries: defaultMaxRetries,
    }

    // Act
    err := config.Validate()

    // Assert
    assert.NoError(t, err, "valid config should pass validation")
}

func TestValidateConfig_WithEmptyPath_ReturnsError(t *testing.T) {
    // Arrange
    config := &Config{
        Path:       "", // Invalid
        Port:       defaultPort,
        MaxRetries: defaultMaxRetries,
    }

    // Act
    err := config.Validate()

    // Assert
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "path cannot be empty")
}

Key Benefits

  1. Maintainability

    • Easier to update and modify tests
    • Clear structure for adding new tests
    • Reduced code duplication
  2. Readability

    • Clear test intentions
    • Well-organized code
    • Consistent patterns
  3. Reliability

    • Thorough error testing
    • Consistent assertions
    • Proper test isolation
  4. Efficiency

    • Reusable test components
    • Reduced boilerplate
    • Faster test writing

Conclusion

These improvements make the test code:

  • More maintainable
  • Easier to understand
  • More reliable
  • More efficient to extend

The patterns and principles can be applied across different types of tests to create a consistent and effective testing strategy.