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.