Testing in Go
Introduction
The Go standard library provides a testing framework with all the tools required to write robust unit tests.
You can read the documentation for the testing package here, but, as you'll see, unit testing in Go follows the language's guiding principle: simple.
Concepts
Key rules and conventions
- Test files must be named with a
_test.gosuffix - Test functions must start with
Test(case matters) - Test functions must have the signature
func TestXxx(t *testing.T) - By convention, tests are placed beside the code they test, e.g. in the example above,
calc.goandcalc_test.gowould be in the same directory.
Table-driven testing
It's common to write Go unit tests with a table-driven approach. Here, a test provides multiple values to test along with expected results. It's likely that if you ask Copilot to generate tests, it will create tests using this approach. See below for an example.
t.Error, t.Fail, and t.Fatal
t.Fail()marks the test as failed, but does not log a message.t.Error(...)logs a message and marks the test as failed.t.Fatal(...)logs a message, marks the test as failed, and stops the current test immediately.
Use t.Error when a test can continue after a failed check, and t.Fatal when continuing would make the rest of the test invalid.
Examples
package calc
// Add is a simple function adding its two arguments
// and returning the result.
func Add(number1, number2 int) int {
return number1 + number2
}
package calc
import (
"testing"
)
// Remember to name your test functions with TestXxx
func TestAddSimple(t *testing.T) {
got := Add(1, 2)
wanted := 3
if got != wanted {
t.Error("1 + 2 should equal 3")
}
}
func TestAddTableDriven(t *testing.T) {
tests := []struct {
name string
first, second int
wanted int
}{
{"positive numbers", 2, 3, 5},
{"zeros", 0, 0, 0},
{"negative numbers", -2, -3, -5},
{"mixed signs", -2, 3, 1},
{"large numbers", 1000000, 2000000, 3000000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.first, tt.second)
if got != tt.wanted {
t.Errorf("Add(%d, %d) = %d, wanted %d", tt.first, tt.second, got, tt.wanted)
}
})
}
}
func helperFunction() {
// helper functions are fine, but only TestXxx functions are executed by go test
}
Then run the go test command in a terminal.
$ go test ./...
ok tests 0.032s
go test ./... will find all test files in the current and nested directories, then run all test functions that it finds.
Challenges
1. Write a simple test for a function that Copilot creates
- Create a new package called
spaces - Add a new file called
spaces.goand declarepackage spacesat the top of the file - Open Copilot Chat and ask: Write a function in Go that counts the spaces in a string. Call the function CountSpaces. It will take a string argument and return an integer result.
- Write a unit test for the new function that tests a single string argument
- Run
go testand check the output
💡 Show Answer
package spaces
import (
"testing"
)
func TestCountSpaces(t *testing.T) {
got := CountSpaces("there should be 4 spaces")
wanted := 4
if got != wanted {
t.Error("expected there to be 4 spaces")
}
}
go test ./...
2. Ask Copilot to write a table-driven test for the CountSpaces function
- Open Copilot Chat and ask: Add a table-driven test for the CountSpaces function and call it TestCountSpacesTableDriven.
- Run
go testand check the output
💡 Show Answer
Copilot will suggest something like the following:
func TestCountSpacesTableDriven(t *testing.T) {
tests := []struct {
name string
input string
want int
}{
{"empty string", "", 0},
{"no spaces", "hello", 0},
{"single space", "hello world", 1},
{"multiple spaces", "one two three", 2},
{"only spaces", " ", 3},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := CountSpaces(tt.input)
if got != tt.want {
t.Errorf("CountSpaces(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
3. Add some scenarios to the table-driven test
- Add the following scenarios to the
testsslice - Check that the function counts a leading space
- Check that the function counts a trailing space
- Check that tabs are not counted
- Run
go testand check the output
💡 Show Answer
Add the following entries to the tests slice:
{"leading space", " hello", 1},
{"trailing space", "hello ", 1},
{"tabs not counted", "hello\tworld", 0},
4. Compare t.Fail, t.Error, and t.Fatal
- Ask Copilot: Show me the difference between t.Fail, t.Error, and t.Fatal in one test file with examples.
- Add three tests named
TestWithFail,TestWithError, andTestWithFatal. - Run
go test -vand observe which log lines appear for each test.
💡 Show Answer
package spaces
import "testing"
func TestWithFail(t *testing.T) {
t.Log("before t.Fail")
t.Fail()
t.Log("after t.Fail") // still runs
}
func TestWithError(t *testing.T) {
t.Log("before t.Error")
t.Error("something was wrong")
t.Log("after t.Error") // still runs
}
func TestWithFatal(t *testing.T) {
t.Log("before t.Fatal")
t.Fatal("cannot continue")
t.Log("after t.Fatal") // unreachable
}
t.Fail and t.Error both allow the current test to continue. t.Fatal stops the current test function immediately.
5. Measure coverage and identify missing test cases
- Run
go test -cover ./...and note the coverage percentage. - Ask Copilot: Based on my CountSpaces tests, what edge cases might still be missing?
- Add at least one new case that increases confidence, then run coverage again.
💡 Show Answer
go test -cover ./...
You can also generate a detailed report:
go test -coverprofile=cover.out ./...
go tool cover -func=cover.out
Possible extra test cases:
{"multiple consecutive spaces", "a b c", 5},
{"space with punctuation", "hello, world !", 2},
6. Stretch: add a benchmark for CountSpaces
- Ask Copilot: Create a Go benchmark called BenchmarkCountSpaces for my CountSpaces function.
- Run
go test -bench=. ./.... - Increase the input string size and compare benchmark results.
💡 Show Answer
package spaces
import (
"strings"
"testing"
)
func BenchmarkCountSpaces(b *testing.B) {
input := strings.Repeat("word ", 1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = CountSpaces(input)
}
}
go test -bench=. ./...
7. Stretch: add a fuzz test
- Ask Copilot: Create a fuzz test for CountSpaces called FuzzCountSpaces.
- Seed it with a few initial strings.
- Run
go test -fuzz=Fuzz -run=^$and let it execute for a short period.
💡 Show Answer
package spaces
import (
"strings"
"testing"
)
func FuzzCountSpaces(f *testing.F) {
seeds := []string{"", "hello", "hello world", " leading and trailing ", "tabs\tand spaces "}
for _, s := range seeds {
f.Add(s)
}
f.Fuzz(func(t *testing.T, input string) {
got := CountSpaces(input)
want := strings.Count(input, " ")
if got != want {
t.Fatalf("CountSpaces(%q) = %d, want %d", input, got, want)
}
})
}
go test -fuzz=Fuzz -run=^$