1.7.1 单元测试基础

1.7.1 单元测试基础 #

单元测试是软件测试的基础,它专注于测试代码中的最小可测试单元。Go 语言内置了完整的测试框架,使得编写和运行测试变得简单而直观。

Go 测试框架概述 #

Go 的测试框架基于以下几个核心概念:

  • 测试文件 - 以 _test.go 结尾的文件
  • 测试函数 - 以 Test 开头的函数
  • testing 包 - 提供测试相关的工具和断言
  • go test 命令 - 运行测试的命令行工具

基本测试结构 #

让我们从一个简单的例子开始:

// math_utils.go
package main

// Add 计算两个整数的和
func Add(a, b int) int {
    return a + b
}

// Multiply 计算两个整数的乘积
func Multiply(a, b int) int {
    return a * b
}

// Divide 计算两个数的除法,返回结果和错误
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

对应的测试文件:

// math_utils_test.go
package main

import (
    "testing"
)

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5

    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

func TestMultiply(t *testing.T) {
    result := Multiply(4, 5)
    expected := 20

    if result != expected {
        t.Errorf("Multiply(4, 5) = %d; want %d", result, expected)
    }
}

func TestDivide(t *testing.T) {
    // 测试正常情况
    result, err := Divide(10, 2)
    if err != nil {
        t.Errorf("Divide(10, 2) returned error: %v", err)
    }

    expected := 5.0
    if result != expected {
        t.Errorf("Divide(10, 2) = %f; want %f", result, expected)
    }

    // 测试除零错误
    _, err = Divide(10, 0)
    if err == nil {
        t.Error("Divide(10, 0) should return error")
    }
}

测试函数的命名规则 #

测试函数必须遵循特定的命名规则:

  1. 函数名 - 必须以 Test 开头
  2. 参数 - 必须接受 *testing.T 类型的参数
  3. 可见性 - 函数名首字母必须大写(公开函数)
// 正确的测试函数命名
func TestUserRegistration(t *testing.T) { /* ... */ }
func TestEmailValidation(t *testing.T) { /* ... */ }
func TestPasswordEncryption(t *testing.T) { /* ... */ }

// 错误的命名(不会被识别为测试函数)
func testUserLogin(t *testing.T) { /* ... */ }  // 首字母小写
func UserTest(t *testing.T) { /* ... */ }       // 不以Test开头
func TestUser() { /* ... */ }                   // 缺少参数

testing.T 类型的常用方法 #

testing.T 提供了丰富的方法来处理测试结果:

错误报告方法 #

func TestErrorMethods(t *testing.T) {
    // Error 和 Errorf - 报告错误但继续执行
    if condition {
        t.Error("Something went wrong")
        t.Errorf("Expected %d, got %d", expected, actual)
    }

    // Fatal 和 Fatalf - 报告错误并立即停止测试
    if criticalCondition {
        t.Fatal("Critical error occurred")
        t.Fatalf("Cannot continue: %v", err)
    }

    // Fail 和 FailNow - 标记测试失败
    if shouldFail {
        t.Fail()    // 标记失败但继续执行
        t.FailNow() // 标记失败并立即停止
    }
}

日志输出方法 #

func TestLoggingMethods(t *testing.T) {
    // Log 和 Logf - 输出日志信息
    t.Log("This is a log message")
    t.Logf("Processing item %d of %d", i, total)

    // 只有在测试失败或使用 -v 标志时才会显示日志
}

跳过测试 #

func TestSkipExample(t *testing.T) {
    if runtime.GOOS == "windows" {
        t.Skip("Skipping test on Windows")
    }

    // 条件跳过
    if !hasRequiredFeature() {
        t.Skipf("Skipping test: required feature not available")
    }

    // 测试代码...
}

表格驱动测试 #

表格驱动测试是 Go 中常用的测试模式,特别适合测试多个输入输出组合:

func TestAddTableDriven(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -1, -2, -3},
        {"mixed numbers", -5, 10, 5},
        {"zero values", 0, 0, 0},
        {"with zero", 5, 0, 5},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

复杂数据结构的测试 #

type User struct {
    ID    int
    Name  string
    Email string
    Age   int
}

func ValidateUser(user User) error {
    if user.Name == "" {
        return errors.New("name cannot be empty")
    }
    if user.Age < 0 {
        return errors.New("age cannot be negative")
    }
    if !strings.Contains(user.Email, "@") {
        return errors.New("invalid email format")
    }
    return nil
}

func TestValidateUser(t *testing.T) {
    tests := []struct {
        name    string
        user    User
        wantErr bool
        errMsg  string
    }{
        {
            name: "valid user",
            user: User{ID: 1, Name: "John", Email: "[email protected]", Age: 25},
            wantErr: false,
        },
        {
            name: "empty name",
            user: User{ID: 2, Name: "", Email: "[email protected]", Age: 30},
            wantErr: true,
            errMsg: "name cannot be empty",
        },
        {
            name: "negative age",
            user: User{ID: 3, Name: "Jane", Email: "[email protected]", Age: -5},
            wantErr: true,
            errMsg: "age cannot be negative",
        },
        {
            name: "invalid email",
            user: User{ID: 4, Name: "Bob", Email: "invalid-email", Age: 20},
            wantErr: true,
            errMsg: "invalid email format",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateUser(tt.user)

            if tt.wantErr {
                if err == nil {
                    t.Errorf("ValidateUser() error = nil, wantErr %v", tt.wantErr)
                    return
                }
                if err.Error() != tt.errMsg {
                    t.Errorf("ValidateUser() error = %v, want %v", err.Error(), tt.errMsg)
                }
            } else {
                if err != nil {
                    t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
                }
            }
        })
    }
}

子测试和并行测试 #

子测试 #

使用 t.Run() 可以创建子测试,提供更好的测试组织和错误报告:

func TestStringOperations(t *testing.T) {
    t.Run("ToUpper", func(t *testing.T) {
        result := strings.ToUpper("hello")
        if result != "HELLO" {
            t.Errorf("ToUpper failed: got %s, want HELLO", result)
        }
    })

    t.Run("ToLower", func(t *testing.T) {
        result := strings.ToLower("WORLD")
        if result != "world" {
            t.Errorf("ToLower failed: got %s, want world", result)
        }
    })

    t.Run("Contains", func(t *testing.T) {
        if !strings.Contains("hello world", "world") {
            t.Error("Contains failed: should find 'world' in 'hello world'")
        }
    })
}

并行测试 #

使用 t.Parallel() 可以让测试并行运行,提高测试执行效率:

func TestParallelOperations(t *testing.T) {
    t.Run("Operation1", func(t *testing.T) {
        t.Parallel()
        // 这个测试会并行运行
        time.Sleep(100 * time.Millisecond)
        // 测试逻辑...
    })

    t.Run("Operation2", func(t *testing.T) {
        t.Parallel()
        // 这个测试也会并行运行
        time.Sleep(100 * time.Millisecond)
        // 测试逻辑...
    })
}

测试辅助函数 #

创建辅助函数可以减少测试代码的重复:

// 测试辅助函数
func assertEqual(t *testing.T, got, want interface{}) {
    t.Helper() // 标记为辅助函数,错误报告会指向调用者
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

func assertError(t *testing.T, err error, wantErr bool) {
    t.Helper()
    if (err != nil) != wantErr {
        t.Errorf("error = %v, wantErr %v", err, wantErr)
    }
}

// 使用辅助函数的测试
func TestWithHelpers(t *testing.T) {
    result := Add(2, 3)
    assertEqual(t, result, 5)

    _, err := Divide(10, 0)
    assertError(t, err, true)
}

运行测试 #

基本命令 #

# 运行当前包的所有测试
go test

# 运行指定包的测试
go test ./package/path

# 运行所有包的测试
go test ./...

# 显示详细输出
go test -v

# 运行特定的测试函数
go test -run TestAdd

# 运行匹配模式的测试
go test -run "TestAdd|TestMultiply"

高级选项 #

# 显示测试覆盖率
go test -cover

# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

# 并行运行测试
go test -parallel 4

# 设置超时时间
go test -timeout 30s

# 运行基准测试
go test -bench=.

# 多次运行测试检查稳定性
go test -count=10

测试最佳实践 #

1. 测试命名 #

// 好的测试命名 - 描述性强
func TestUserService_CreateUser_WithValidData_ReturnsUser(t *testing.T) {}
func TestEmailValidator_ValidateEmail_WithInvalidFormat_ReturnsError(t *testing.T) {}

// 避免的命名 - 过于简单
func TestUser(t *testing.T) {}
func TestValidate(t *testing.T) {}

2. 测试组织 #

func TestUserService(t *testing.T) {
    // 使用子测试组织相关测试
    t.Run("CreateUser", func(t *testing.T) {
        t.Run("WithValidData", func(t *testing.T) {
            // 测试逻辑
        })
        t.Run("WithInvalidData", func(t *testing.T) {
            // 测试逻辑
        })
    })

    t.Run("UpdateUser", func(t *testing.T) {
        // 更新用户的测试
    })
}

3. 测试数据准备 #

func TestUserOperations(t *testing.T) {
    // 准备测试数据
    validUser := User{
        ID:    1,
        Name:  "Test User",
        Email: "[email protected]",
        Age:   25,
    }

    // 使用表格驱动测试处理多种情况
    tests := []struct {
        name string
        user User
        want bool
    }{
        {"valid user", validUser, true},
        {"invalid user", User{}, false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // 测试逻辑
        })
    }
}

4. 错误处理测试 #

func TestErrorHandling(t *testing.T) {
    // 测试错误情况
    _, err := SomeFunction("invalid input")
    if err == nil {
        t.Error("Expected error for invalid input, got nil")
    }

    // 测试特定错误类型
    var expectedErr *CustomError
    if !errors.As(err, &expectedErr) {
        t.Errorf("Expected CustomError, got %T", err)
    }
}

通过掌握这些单元测试基础知识,您已经具备了编写高质量测试代码的能力。在下一节中,我们将学习如何使用测试覆盖率工具来评估测试质量,以及如何编写性能基准测试。