单元测试基础 #

测试是保证代码质量的重要手段。Go语言内置了轻量级的测试框架,通过 testing 包可以轻松编写和运行测试。

测试基础 #

测试文件命名 #

测试文件以 _test.go 结尾,与被测试文件放在同一目录:

text
myapp/
├── calculator.go
└── calculator_test.go

第一个测试 #

go
package myapp

import "testing"

func Add(a, b int) int {
    return a + b
}

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

运行测试 #

bash
go test                    # 运行当前包的测试
go test -v                 # 详细输出
go test ./...              # 运行所有包的测试
go test -run TestAdd       # 运行特定测试

测试函数 #

测试函数签名 #

测试函数必须以 Test 开头,参数为 *testing.T

go
package myapp

import "testing"

func TestFunctionName(t *testing.T) {
}

func TestAnotherFunction(t *testing.T) {
}

报告失败 #

go
package myapp

import "testing"

func TestExample(t *testing.T) {
    got := 2 + 2
    want := 4
    
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

func TestFatal(t *testing.T) {
    if true == false {
        t.Fatal("这个错误会立即终止测试")
    }
}

失败报告方法:

方法 说明
t.Error() 记录错误,继续执行
t.Errorf() 格式化记录错误,继续执行
t.Fatal() 记录错误,立即终止
t.Fatalf() 格式化记录错误,立即终止
t.Skip() 跳过当前测试
t.Skipf() 格式化跳过当前测试

表格驱动测试 #

表格驱动测试是Go推荐的测试模式,可以方便地测试多种情况:

go
package myapp

import "testing"

func Add(a, b int) int {
    return a + b
}

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"正数相加", 2, 3, 5},
        {"负数相加", -2, -3, -5},
        {"正负混合", 2, -3, -1},
        {"零值测试", 0, 0, 0},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d", 
                    tt.a, tt.b, got, tt.expected)
            }
        })
    }
}

运行子测试 #

bash
go test -v -run TestAdd/正数相加

测试辅助函数 #

使用 t.Helper() #

标记辅助函数,使错误报告指向正确的行号:

go
package myapp

import "testing"

func assertEqual(t *testing.T, got, want int) {
    t.Helper()
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

func TestWithHelper(t *testing.T) {
    assertEqual(t, 2+2, 4)
    assertEqual(t, 3+3, 6)
}

测试清理 #

go
package myapp

import "testing"

func TestWithCleanup(t *testing.T) {
    t.Cleanup(func() {
        t.Log("清理资源")
    })
    
    t.Log("执行测试")
}

测试初始化 #

TestMain #

TestMain 用于测试前后的初始化和清理:

go
package myapp

import (
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    os.Setenv("TEST_MODE", "true")
    
    code := m.Run()
    
    os.Unsetenv("TEST_MODE")
    os.Exit(code)
}

测试覆盖率 #

生成覆盖率报告 #

bash
go test -cover
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

代码示例 #

go
package myapp

func Abs(x int) int {
    if x < 0 {
        return -x
    }
    return x
}
go
package myapp

import "testing"

func TestAbs(t *testing.T) {
    tests := []struct {
        input    int
        expected int
    }{
        {5, 5},
        {-5, 5},
        {0, 0},
    }
    
    for _, tt := range tests {
        got := Abs(tt.input)
        if got != tt.expected {
            t.Errorf("Abs(%d) = %d; want %d", 
                tt.input, got, tt.expected)
        }
    }
}

测试浮点数 #

go
package myapp

import (
    "math"
    "testing"
)

func TestFloatEqual(t *testing.T) {
    got := 0.1 + 0.2
    want := 0.3
    
    if math.Abs(got-want) > 1e-9 {
        t.Errorf("got %f, want %f", got, want)
    }
}

测试错误处理 #

go
package myapp

import (
    "errors"
    "testing"
)

func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func TestDivide(t *testing.T) {
    t.Run("正常除法", func(t *testing.T) {
        got, err := Divide(10, 2)
        if err != nil {
            t.Fatalf("unexpected error: %v", err)
        }
        if got != 5 {
            t.Errorf("got %f, want 5", got)
        }
    })
    
    t.Run("除零错误", func(t *testing.T) {
        _, err := Divide(10, 0)
        if err == nil {
            t.Error("expected error but got nil")
        }
    })
}

临时目录和文件 #

go
package myapp

import (
    "os"
    "path/filepath"
    "testing"
)

func TestWithTempDir(t *testing.T) {
    tempDir := t.TempDir()
    
    filename := filepath.Join(tempDir, "test.txt")
    err := os.WriteFile(filename, []byte("test content"), 0644)
    if err != nil {
        t.Fatal(err)
    }
    
    content, err := os.ReadFile(filename)
    if err != nil {
        t.Fatal(err)
    }
    
    if string(content) != "test content" {
        t.Errorf("unexpected content: %s", content)
    }
}

实用示例:测试结构体方法 #

go
package myapp

import "testing"

type Calculator struct{}

func (c *Calculator) Add(a, b int) int {
    return a + b
}

func (c *Calculator) Subtract(a, b int) int {
    return a - b
}

func (c *Calculator) Multiply(a, b int) int {
    return a * b
}

func (c *Calculator) Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, ErrDivisionByZero
    }
    return a / b, nil
}

var ErrDivisionByZero = errors.New("division by zero")

func TestCalculator(t *testing.T) {
    calc := &Calculator{}
    
    t.Run("Add", func(t *testing.T) {
        tests := []struct {
            a, b, want int
        }{
            {1, 2, 3},
            {-1, 1, 0},
            {0, 0, 0},
        }
        
        for _, tt := range tests {
            got := calc.Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) = %d; want %d", 
                    tt.a, tt.b, got, tt.want)
            }
        }
    })
    
    t.Run("Divide", func(t *testing.T) {
        t.Run("正常除法", func(t *testing.T) {
            got, err := calc.Divide(10, 2)
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if got != 5 {
                t.Errorf("got %d, want 5", got)
            }
        })
        
        t.Run("除零错误", func(t *testing.T) {
            _, err := calc.Divide(10, 0)
            if err != ErrDivisionByZero {
                t.Errorf("expected ErrDivisionByZero, got %v", err)
            }
        })
    })
}

小结 #

命令 说明
go test 运行测试
go test -v 详细输出
go test -run 运行匹配的测试
go test -cover 显示覆盖率
go test -coverprofile 生成覆盖率报告

单元测试是保证代码质量的基础,良好的测试习惯可以大大减少bug数量,提高代码可维护性。