Tuesday, February 10, 2026

Testing

    Profile Pic of Akash AmanAkash Aman

    Updated: February 2026

    Testing in Go

    The Testing Convention (*_test.go)

    Concept: Go uses a strict naming convention to distinguish between production code and test code.

    Setup:

    • Project structure
    bash
    math/
    β”œβ”€β”€ add.go       # Production logic
    └── add_test.go  # Test logic
    
    • Behavior: Files ending in _test.go are excluded from the regular build. They are only compiled and executed when you run go test.
    • Why: This ensures your production binaries remain slim and free of test-related overhead while keeping tests physically close to the code they verify.

    Test Package Strategy (package math_test)

    Concept: Choosing whether to test "inside" the package or "outside" as a consumer.

    • Behavior:

    • Internal Tests (package math): Can access private (unexported) variables and functions.

    • External Tests (package math_test): Can only access public (exported) APIs.

    • Why: Using _test in the package name (e.g., package math_test) forces you to write Black-Box Tests. This ensures you are testing the "behavior" (what the code does) rather than the "implementation" (how it does it), leading to more decoupled and maintainable code.

    The Test Entry Point (func TestXxx(t *testing.T))

    Concept: A standardized function signature that the Go test runner identifies and executes.

    • Behavior: Functions must start with Test, followed by a capitalized letter, and accept a pointer to testing.T.
    go
    func TestAdd(t *testing.T) {
        got := math.Add(2, 2)
        want := 4
    
        if got != want {
            t.Errorf("got %d, want %d", got, want)
        }
    }
    
    • Why: Using testing.T provides methods like Errorf (which marks a test as failed but continues execution) or Fatalf (which stops the test immediately). This gives you granular control over the test lifecycle.

    Table-Driven Tests

    Concept: Defining a collection of inputs and expected results in a slice of structs to run multiple scenarios through a single test loop.

    • Behavior: It treats the test logic as a generic "runner" and the data as "configuration."
    go
    func TestAdd(t *testing.T) {
        // Setup: Table of scenarios
        tests := []struct {
            name string
            a, b int
            want int
        }{
            {"positive numbers", 2, 2, 4},
            {"negative numbers", -1, -1, -2},
            {"zero", 0, 0, 0},
        }
    
        for _, tt := range tests {
            t.Run(tt.name, func(t *testing.T) { // Concept: Sub-tests
                if got := math.Add(tt.a, tt.b); got != tt.want {
                    t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
                }
            })
        }
    }
    
    • Why: This is the idiomatic Go way to test. It eliminates code duplication and makes it extremely easy to add edge cases without writing new functions.

    Code Coverage (-coverprofile)

    Concept: A statistical measure of how many lines of your production code were actually executed during tests.

    • Behavior: Go calculates this during runtime. You can generate a report and view it in a browser.
    bash
    # Generate profile
    go test -coverprofile=coverage.out ./...
    
    # View as HTML
    go tool cover -html=coverage.out
    
    • Why: It identifies "dark corners" of your codebaseβ€”logic paths (like error handling) that you might have forgotten to test.

    Fuzz Testing (testing.F)

    Concept: An automated testing technique that generates random, mutated data to find edge cases humans might overlook.

    • Behavior: Instead of providing static values, you provide a "seed corpus" and let Go generate thousands of random inputs to find inputs that cause crashes or panics.
    go
    func FuzzAdd(f *testing.F) {
        f.Add(5, 5) // Seed corpus
        f.Fuzz(func(t *testing.T, a, b int) {
            math.Add(a, b)
        })
    }
    
    • Why: Essential for security-sensitive or complex logic. Fuzzing can catch integer overflows, buffer boundaries, or nil-pointer dereferences that static test cases usually miss.

    Reference Guides

    Crucial Commands

    CommandWhat it actually does
    go test ./...Recursively runs all tests in the project.
    go test -vVerbose: Lists every test case as it runs.
    go test -run <Name>Runs only the tests that match the specific regex name.
    go test -fuzz .Starts the fuzzing engine to find automated edge cases.
    go clean -testcacheForces a fresh run by clearing the cached test results.

    Summary Table

    ConceptPurposeDeveloper Impact
    *_test.goIsolationSeparates test logic from production code.
    Table-DrivenScalabilityMakes adding new test cases a matter of adding one line.
    Sub-testsPrecisionAllows running and failing individual cases within a loop.
    CoverageQualityProvides a visual map of untested code.
    FuzzingRobustnessFinds "unthinkable" bugs via automated data mutation.

    Β© 2026 Akash Aman | All rights reserved