Clean code in Go · 3 MIN READ

Behavior tests: tables and boundary cases

Test an observable contract with success and failure inputs.

Test an observable contract with success and failure inputs. A table of cases makes differences explicit without copying the whole test body. Trace the worked Go example, then compare the benefit with the cost before applying this design to your own project.

Clean Go · A MAP OF THE IDEA

  1. Choose inputs that challenge the contract
  2. Run the same observable operation
  3. Compare values and error categories

Useful when

  • Several boundary cases share the same behavior
  • Refactoring should preserve externally visible results

Watch out

  • Mocks can mirror implementation details
  • Fakes cannot prove a real database adapter works

Implementation & operation cost: Low

01

The design decision

A table of cases makes differences explicit without copying the whole test body. Include edge cases that can falsify the contract, not just another happy path. A fake should model behavior at a boundary rather than assert every internal call in a fixed order.

02

A complete Go example

Read this program from the entry point and follow the calls shown in the diagram. The output is included at the end so you can check the behavior without a compiler.

go
package main

import (
	"errors"
	"fmt"
	"strings"
)

func Normalize(s string) (string, error) {
	s = strings.TrimSpace(s)
	if s == "" {
		return "", errors.New("empty name")
	}
	return strings.ToLower(s), nil
}

func main() {
	cases := []struct {
		input, want string
		fail        bool
	}{
		{" Go ", "go", false}, {"", "", true}, {"  ", "", true},
	}
	for _, tc := range cases {
		got, err := Normalize(tc.input)
		if got != tc.want || (err != nil) != tc.fail {
			panic("contract failed")
		}
	}
	fmt.Println("3 cases passed")
}

// Output:
// 3 cases passed
Read the code

The cases distinguish trimmed content from two forms of empty input and verify both the value and error presence. In a _test.go file, place the loop in TestNormalize and report the case with t.Errorf or t.Run rather than panic.

03

Tradeoffs and the simpler alternative

Add integration tests where real serialization, SQL or network contracts matter. Table shape alone does not make a test meaningful. Fuzz properties such as round-trip encoding or no panics when the input space is larger than the useful handpicked examples.

04

Derive cases from the behavior contract

For a transfer, cover success, insufficient balance, missing destination, cancellation and a durable-write failure. Include a boundary such as zero or maximum allowed amount. A table is an organization tool, not a reason to make unrelated assertions share a giant struct. Give each case a name describing the behavior, and compare the public result and durable effect rather than private helper calls.

05

Use real integration points where fakes cannot prove behavior

A fake repository can verify application decisions, but it cannot prove a SQL transaction rolls back or an index enforces uniqueness. Keep a focused adapter contract test with the real database in a controlled environment. Avoid sleeping to guess when concurrent work finished; synchronize using channels or an observable completion boundary. Call t.Parallel only when the case owns isolated state and does not mutate shared environment or fixtures.

06

Treat nondeterminism as a design signal

Inject time and randomness where the caller needs a predictable boundary, and wait for owned goroutines before a test returns. The race detector finds executions with conflicting accesses; one clean run does not prove every possible schedule safe. A benchmark needs representative input and allocation measurements, not an architecture score. Keep a failing regression for the user-visible defect while resisting tests that merely restate the implementation line by line.

TAKE THIS WITH YOU

Checks results and errors against a public contract. Add integration tests where real serialization, SQL or network contracts matter. Table shape alone does not make a test meaningful. Fuzz properties such as round-trip encoding or no panics when the input space is larger than the useful handpicked examples.

Check your understanding

Which statement best describes Behavior tests: tables and boundary cases?

Go to the source

Progress stays in this browser. No account needed.