Unit tests in Go can be written using testing
package. We require to create a file with _test.go
suffix to write tests for a package. Conventionally, the test file should be in the same package as the code being tested. The test file should import the testing
package and the package being tested.
eg:
.
├── main.go
└── main_test.go
Writing Tests
Suppose we have a function Add
in main.go
which adds two numbers.
// main.go
package main
func Add(a, b int) int {
return a + b
}
We can write a test for this function in main_test.go
.
// main_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
Running Tests
To run the tests, use the go test
command.
go test ./... # runs all the test files
To run specific test files, use go test -run
command.
go test -run main_test.go
Writing Benchmarks
Benchmarks can be written to measure the performance of your code. Benchmarks are written using the Benchmark
function from the testing
package.
// main_test.go
package main
import "testing"
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
To run the benchmarks, use the -bench
flag with the go test
command.
go test -bench .
Table-Driven Tests
Table-driven tests are a way to test a function with multiple inputs and expected outputs. This is done by creating a slice of structs, where each struct contains the input and expected output.
// main_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
a, b, want int
}{
{2, 3, 5},
{5, 7, 12},
{0, 0, 0},
{-1, 1, 0},
}
for _, tt := range tests {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
}
}
}
Subtests
Subtests are a way to group tests and run them together. This is useful when you want to test different scenarios for a function.
// main_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
t.Run("Add positive numbers", func(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
})
t.Run("Add negative numbers", func(t *testing.T) {
result := Add(-2, -3)
if result != -5 {
t.Errorf("Add(-2, -3) = %d; want -5", result)
}
})
t.Run("Add zero", func(t *testing.T) {
result := Add(0, 0)
if result != 0 {
t.Errorf("Add(0, 0) = %d; want 0", result)
}
})
}
Mocking
Mocking is a common practice to mock dependencies in unit tests. This is done by creating a mock implementation of the dependency and using it in the test.
// main.go
package main
// suppose we have a database interface and implementation
type Database interface {
Get(key string) string
Set(key, value string)
}
// a function that uses the database
func GetFromDatabase(db Database, key string) string {
return db.Get(key)
}
// main_test.go
package main
import "testing"
type MockDatabase struct{}
// This will be used in the test
func (m *MockDatabase) Get(key string) string {
return "mock value"
}
func TestGetFromDatabase(t *testing.T) {
db := &MockDatabase{}
result := GetFromDatabase(db, "key")
if result != "mock value" {
t.Errorf("GetFromDatabase(db, \"key\") = %s; want \"mock value\"", result)
}
}
Mocking package stretchr/testify
provides a mock
package which can be used to create mock implementations of interfaces. We can also use mockery
cli tool to generate mocks for interfaces. Using this package allows us to generate more dynamic mocks and also provides more control over the mock behavior.