Created: 2026-05-23 Updated: 2026-05-24 8 min read

Unit testing is over hyped

package main

import "fmt"

func DoWork() {
    go func() {
        result := 42 * 2
        fmt.Println(result)
    }()
}

func TestDoWork(t *testing.T) {
    DoWork()
    // Now what?
    // The goroutine runs independently
    // You have no handle on result
    // You can't assert anything
    // The function already returned
}

To get any handle on the result you have to change the API. Sometimes you’re changing the API purely for testability. That’s the modularity creep problem: the test is quietly shaping the design.

func DoWork() chan int {
    ch := make(chan int, 1)
    go func() {
        ch <- 42 * 2
    }()
    return ch
}

func Add(a, b int) int {
    return a + b
}

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("expected 5, got %d", result)
    }
}

You refactor Add to take a struct with same behaviour, different signature:

type AddInput struct {
    A, B int
}

func Add(input AddInput) int {
    return input.A + input.B
}

Now TestAdd breaks even though the logic didn’t change. With high coverage you get hundreds of these. One pattern that helps is the adapter patternyour test calls a stable adapter, and only the adapter needs updating when the internal signature changes:

// stable signature the tests always call
func AddAdapter(inputs []int) int {
    return Add(AddInput{A: inputs[0], B: inputs[1]})
}

func TestAdd(t *testing.T) {
    result := AddAdapter([]int{2, 3})
    if result != 5 {
        t.Errorf("expected 5, got %d", result)
    }
}

Refactor Add however you want only AddAdapter needs updating, not every test. But now you’re maintaining extra scaffolding purely to keep tests from breaking.