Why Testing in Go Is Not Scary, But Necessary
\n\nWhen you first write in Go, it seems the compiler catches almost everything: types are strict, errors are explicit, nil is under control. But as soon as your project grows to 500+ lines, you realize: you can't live without tests. One change in one place breaks another, and you only find out about it in production.
In this article, I'll show a beginner developer where to start with testing in Go. We'll cover unit tests, table-driven tests, mocks, and code coverage. No fluff — only working examples and practical advice.
\n\nIf you've been writing Go for at least a couple of months, this material will save you hours of debugging.
\n\nWhat You Need to Know Before Starting
\n\nGo provides a built-in testing package — nothing extra needs to be installed. Tests are written in files with the _test.go suffix and run with the command:
go test ./...\n\nBasic rules:
\n- \n
- The test function must be named
TestXxx(t *testing.T), where Xxx is the name of the function being tested, starting with a capital letter. \n - Test files are placed next to the code being tested. \n
- Errors are reported via
t.Error()ort.Fatal(). \n
Let's jump straight into practice.
\n\nWriting Your First Unit Test in Go
\n\nLet's say we have a simple function that sums two numbers:
\n\n// calc.go\npackage calc\n\nfunc Sum(a, b int) int {\n return a + b\n}\n\nCreate a file calc_test.go in the same folder:
// calc_test.go\npackage calc\n\nimport "testing"\n\nfunc TestSum(t *testing.T) {\n result := Sum(2, 3)\n expected := 5\n\n if result != expected {\n t.Errorf("Sum(2,3) = %d; want %d", result, expected)\n }\n}\n\nRun the test:
\n\ngo test -v\n\nYou'll see a green PASS — your first test victory. Note: we use t.Errorf (non-fatal) — the test will continue even if one case fails.
Table-Driven Tests: When One Test Is Inconvenient
\n\nImagine you need to test a function with a dozen different inputs. Write 10 separate tests? No, in Go there are table-driven tests for this. It's a standard in the Go community.
\n\nLet's add a division function to our calc.go:
func Divide(a, b int) (int, error) {\n if b == 0 {\n return 0, errors.New("division by zero")\n }\n return a / b, nil\n}\n\nNow let's write a table-driven test:
\n\nfunc TestDivide(t *testing.T) {\n tests := []struct {\n name string\n a, b int\n expected int\n wantErr bool\n }{\n {"positive", 10, 2, 5, false},\n {"negative", -6, 3, -2, false},\n {"zero division", 5, 0, 0, true},\n {"same numbers", 7, 7, 1, false},\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n got, err := Divide(tt.a, tt.b)\n if tt.wantErr {\n if err == nil {\n t.Error("expected error, got nil")\n }\n return\n }\n if err != nil {\n t.Errorf("unexpected error: %v", err)\n }\n if got != tt.expected {\n t.Errorf("Divide(%d,%d) = %d; want %d", tt.a, tt.b, got, tt.expected)\n }\n })\n }\n}\n\nWhat's great here:
\n- \n
- Each case is a separate subtest with a name. If one fails, the rest still run. \n
- It's easy to add new cases — just add a line to the struct. \n
- The code reads like a specification. \n
Run it with the -v flag — you'll see the names of all subtests. This is very convenient for debugging.