Testing in Go

The Bug That Cost Us $2,847

It was a Tuesday when our billing system double-charged 127 customers. The bug was in our discount calculation:

func calculateDiscount(price float64, couponCode string) float64 {
    discount := 0.0
    
    if couponCode == "SAVE10" {
        discount = price * 0.10
    } else if couponCode == "SAVE20" {
        discount = price * 0.20
    }
    
    // BUG: Forgot to subtract discount!
    return price
}

I'd written this function on Friday, manually tested it once with fmt.Println(), and shipped it. Over the weekend, the bug processed hundreds of transactions.

The damage:

  • $2,847 in refunds

  • 3 hours of support tickets

  • 1 very angry product manager

  • 1 humiliated developer (me)

That Tuesday afternoon, I learned to write tests. Not "eventually" or "if I have time" - first. Before the code even runs.

This article covers the testing practices that prevented every bug since.


Test-Driven Development (TDD)

After the billing disaster, I adopted TDD:

  1. Write the test first (it fails)

  2. Write minimal code to pass the test

  3. Refactor while keeping tests green

The Corrected Version (TDD Style)

Step 1: Write the test

Run it:

Step 2: Write minimal code

Run it:

Step 3: Add more test cases

The bug would never have made it to production.


Testing Basics

File Naming Convention

Test Function Signature

Rules:

  • Function name starts with Test

  • Takes *testing.T parameter

  • File ends with _test.go

Running Tests


Table-Driven Tests

The most idiomatic Go testing pattern.

Basic Pattern

Benefits:

  • Easy to add new test cases

  • Clear test names

  • DRY (Don't Repeat Yourself)

Real Example: Email Validator

Output:


Test Coverage

Measuring Coverage

Example with Coverage Analysis

Test file:

Check coverage:

Add missing tests:

Now:


Benchmarks

Measure performance with benchmarks.

Basic Benchmark

Run it:

Means: 1 billion iterations, 0.25 nanoseconds per operation.

Real Example: String Concatenation

Results:

Builder is 4.6x faster with fewer allocations!


Example Tests

Example tests serve as documentation.

Benefits:

  • Appears in go doc output

  • Verified by go test

  • Serves as documentation


Test Helpers and Utilities

Helper Functions

Setup and Teardown

Subtests

Run specific subtest:


Testify Library

The most popular testing library adds assertions and mocks.

Installation

Assertions

Require (Stops Test on Failure)

Common Assertions


Mocking Strategies

Interface-Based Mocking

Testify Mock


Real Example: Testing HTTP Handlers


Real Project: Complete Test Suite

Here's a test suite from one of my projects:


Your Challenge

Write a complete test suite for a URL shortener:


Key Takeaways

  1. TDD: Write tests first, then code

  2. Table-driven tests: Most idiomatic Go pattern

  3. Coverage: Aim for >80%, but quality > quantity

  4. Benchmarks: Measure performance, compare approaches

  5. Example tests: Executable documentation

  6. Testify: Popular library for assertions and mocks

  7. Interface mocking: Test without dependencies

  8. httptest: Test HTTP handlers easily


What I Learned

That $2,847 billing bug taught me that tests aren't optional:

  • TDD prevented bugs before they reached production

  • Table-driven tests made edge cases obvious

  • 95% coverage caught issues in code review

  • Zero billing bugs in 18 months since adopting TDD

Coming from Python's unittest complexity and JavaScript's Jest configuration hell, Go's testing felt refreshing. No setup. No configuration. Just write tests.

The time saved debugging production issues? Immeasurable. The confidence to refactor fearlessly? Priceless.


Next: JSON and REST APIs

In the next article, we'll build a complete REST API client. You'll learn JSON marshaling, HTTP requests, middleware, and the patterns I used to build production-ready API integrations.

Last updated