DMarlon Studio

Lesson 18 · 18 of 20

Table-driven checks · Go

Check a Go function with a table of cases, just like go test does: each case with a name, an input and an expected result.

Go · Table-driven checks in DMarlon Studio: the example's code and its output in the terminal

The most common way to test a function in Go is a table of cases: a slice of structs with a name, the input and the expected result. tests is a slice of an anonymous struct, declared right there because it is used nowhere else.

The function under test is shipping: orders of 50.00 EUR or more ship free and the rest pay 4.95. Amounts are in cents (5000 is 50.00 EUR) so you compare integers rather than decimals, which may not match because of rounding.

The loop calls shipping with each total, compares what it got (got) with what it expected (want) and prints ok or FAIL. On a failure the message says what was called, what came out and what should have: that is usually enough to find the bug without a debugger. failed counts the failures for the final summary.

And there is one: exactly 50.00 expects 0 but gets 495, because the condition is total > 5000 and should be >=. It is the classic bug, the boundary one, which is why you should always test the edges: one cent short (4999), the limit itself (5000) and the empty case (0).

In a project, this same table goes into a shipping_test.go file, inside func TestShipping(t *testing.T); each case runs with t.Run(tc.name, ...), failures are reported with t.Errorf, and the whole thing runs with go test. Adding a case means adding one line.

Code · main.go

package mainimport "fmt"// shipping returns the delivery cost in cents: orders from 50.00 ship free.func shipping(total int) int {	if total > 5000 { // bug: an order of exactly 50.00 should also ship free		return 0	}	return 495}func main() {	tests := []struct {		name        string		total, want int // in cents	}{		{"small order", 2000, 495},		{"one cent short", 4999, 495},		{"exactly 50.00", 5000, 0},		{"large order", 12000, 0},		{"empty cart", 0, 495},	}	failed := 0	for _, tc := range tests {		got := shipping(tc.total)		status := "ok  "		if got != tc.want {			status, failed = "FAIL", failed+1		}		fmt.Printf("%s %-15s shipping(%5d) = %3d, want %3d\n", status, tc.name, tc.total, got, tc.want)	}	fmt.Printf("%d passed, %d failed\n", len(tests)-failed, failed)}

Output · go run main.go

ok   small order     shipping( 2000) = 495, want 495
ok   one cent short  shipping( 4999) = 495, want 495
FAIL exactly 50.00   shipping( 5000) = 495, want   0
ok   large order     shipping(12000) =   0, want   0
ok   empty cart      shipping(    0) = 495, want 495
4 passed, 1 failed

Key points

  • A table of cases is a slice of structs: name, input and expected.
  • Compare got with want, and show both when it fails.
  • Test the edges: the limit itself, one below and the empty case.
  • With money, compare whole cents.
  • In go test, the same table with t.Run and t.Errorf.

Exercise

Fix the bug by changing > to >= and check that all five cases pass. Then add an express bool parameter that always adds 6.00 EUR, and extend the table with two express cases.