38 KiB
Code Improvements by Claude
Table of Contents
- General Code Organization Principles
- Non-Go Files
- Testing Principles
- Key Benefits
- Conclusion
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
- Make zero values useful
- Keep interfaces small and focused, observing the single responsibility principle
- Observe the open-closed principle so that it is open for extension but closed to modification
- Observe the dependency inversion principle to keep interfaces loosely coupled
- Design for composition over inheritance
- Use option patterns for complex configurations
- Make dependencies explicit
5. Documentation Practices
Go Code Documentation Standards
Following the official Go Documentation Guidelines:
-
Package Documentation
- Every package must have a doc comment immediately before the
packagestatement - 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
- Every package must have a doc comment immediately before the
-
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{}) {} -
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
-
Examples
-
Add examples for complex types or functions using
Examplefunctions -
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 }
-
-
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
-
Quality Control
- Run
go docto verify how documentation will appear - Review documentation during code reviews
- Keep examples up to date and passing
- Update docs when changing behavior
- Run
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
-
Unit Tests
- Test individual components
- Mock dependencies
- Focus on behavior not implementation
-
Integration Tests
- Test component interactions
- Use real dependencies
- Test complete workflows
-
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.Poolfor 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
-
CPU Usage
# Profile for 30 seconds go test -cpuprofile=cpu.prof -bench=. go tool pprof cpu.prof -
Memory Allocations
# Track allocations go test -memprofile=mem.prof -bench=. go tool pprof -alloc_objects mem.prof -
Goroutine Block Profiling
# Track goroutine blocks go test -blockprofile=block.prof -bench=. go tool pprof block.prof -
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
-
CPU Profile
- Hot functions
- Call graph
- Time per call
- Call count
-
Memory Profile
- Allocation count
- Allocation size
- Temporary allocations
- Leak suspects
-
Goroutine Profile
- Active goroutines
- Blocked goroutines
- Scheduling latency
- Stack traces
-
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
idnames 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:
- https://docs.github.com/en/actions/how-tos/create-and-publish-actions/manage-custom-actions
- https://docs.github.com/en/actions/how-tos/create-and-publish-actions/release-and-maintain-actions
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.shwhenever 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
- Good:
- Create scripts in the
scriptsdirectory (nottools) - Make scripts executable (
chmod +x) - Add new functionality to the
scripts/menu.shscript for easy access - Add usage information (viewable with
-hor--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:
-
Test File Structure
- Name test files as
<script_name>_test.sh - Place in
scripts/testsdirectory - Make test files executable (
chmod +x) - Source the common test library (
test_lib.sh)
- Name test files as
-
Common Test Library The
test_lib.shlibrary provides a standard test framework. See thescripts/tests/template_test.shfor examples of how to set up one. -
Test Organization
- Group related test cases into sections
- Test each command/flag combination
- Test error conditions explicitly
-
Test Coverage
- Test error conditions
- Test input validation
- Test edge cases
- Test each supported flag/option
-
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_PATHbefore sourcingtest_lib.shto avoid variable conflicts - Function-based Test Organization: Tests are organized in a
run_tests()function called after framework initialization - Consistent Test Sections: Use
test_sectionto 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/--verboseand-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.shreads.envand updatesgo.modaccordingly- 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:
-
scripts/export_version.sh - Complete implementation:
- ✅ Argument parsing with
--helpand--verbose - ✅ Proper error handling and logging
- ✅ Comprehensive test suite in
scripts/tests/export_version_test.sh - ✅ Function-based test organization
- ✅ Argument parsing with
-
scripts/setup_dev.sh - Complete implementation:
- ✅ Argument parsing with
--helpand--verbose - ✅ Script-specific help documentation
- ✅ Error handling for unknown options
- ✅ Comprehensive test suite in
scripts/tests/setup_dev_test.sh - ✅ Function-based test organization
- ✅ Argument parsing with
-
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
-
Centralized Configuration Management:
- ✅
.envfile as single source of truth for versions - ✅ GitHub Actions CI integration with version propagation
- ✅
scripts/sync_go_version.shfor configuration synchronization
- ✅
🔄 Remaining Scripts to Update:
These scripts need the same pattern implementations:
scripts/distribute.sh- Needs argument parsing and testingscripts/update_md_tocs.sh- Needs argument parsing and testingscripts/check_and_fix_env.sh- Needs argument parsing and testingscripts/template.sh- Needs argument parsing and testingscripts/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)
- Values containing spaces:
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
-
Clear Naming
- Name test data clearly and meaningfully
- Name by abstraction, not implementation
- Use
expectedfor expected values - Use
actualfor 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, andTest<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 } -
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
-
Code Organization
- Group related constants and variables
- Place helper functions at the bottom
- Organize tests by function under test
- Follow arrange-act-assert pattern
-
Test Data Management
- Centralize test data definitions
- Use constants for fixed values
- Abstract complex data arrangement into helpers
-
Error Handling
- Test both success and error cases
- Use clear error messages
- Validate error types and messages
- Handle expected and unexpected errors
-
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
-
Maintainability
- Easier to update and modify tests
- Clear structure for adding new tests
- Reduced code duplication
-
Readability
- Clear test intentions
- Well-organized code
- Consistent patterns
-
Reliability
- Thorough error testing
- Consistent assertions
- Proper test isolation
-
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.