Go Testing from Scratch: A Guide for Beginner Developers

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why Testing in Go Is Not Scary, But Necessary

\n\n

When 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.

\n\n

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\n

If you've been writing Go for at least a couple of months, this material will save you hours of debugging.

\n\n

What You Need to Know Before Starting

\n\n

Go 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:

\n\n
go test ./...
\n\n

Basic 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() or t.Fatal().
  • \n
\n\n

Let's jump straight into practice.

\n\n

Writing Your First Unit Test in Go

\n\n

Let'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\n

Create a file calc_test.go in the same folder:

\n\n
// 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\n

Run the test:

\n\n
go test -v
\n\n

You'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.

\n\n

Table-Driven Tests: When One Test Is Inconvenient

\n\n

Imagine 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\n

Let's add a division function to our calc.go:

\n\n
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\n

Now let's write a table-driven test:

\n\n
func 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\n

What'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
\n\n

Run it with the -v flag — you'll see the names of all subtests. This is very convenient for debugging.

\n\n

Testing

Blogs

Book Recommendations