Go语言中的测试:从单元测试到集成测试

Go语言中的测试:从单元测试到集成测试

作为一个写了十几年代码的Go后端老兵,我深刻体会到测试在应用开发中的重要性。好的测试可以保证代码质量,减少bug,提高代码的可维护性。Go语言内置了强大的测试框架,使得测试变得简单而高效。今天咱们就聊聊Go语言中的测试,从单元测试到集成测试,帮助你写出更可靠的代码。

单元测试

1. 基本用法

Go语言的测试框架使用testing包,测试文件以_test.go结尾。

go 复制代码
// calculator.go
package calculator

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

func Subtract(a, b int) int {
    return a - b
}
go 复制代码
// calculator_test.go
package calculator

import (
    "testing"
)

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

    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)
            }
        })
    }
}

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

2. 表驱动测试

表驱动测试是Go语言中一种常见的测试模式,通过表格形式定义多个测试用例。

go 复制代码
func TestMultiply(t *testing.T) {
    tests := []struct {
        a        int
        b        int
        expected int
    }{
        {0, 0, 0},
        {1, 1, 1},
        {2, 3, 6},
        {-2, 3, -6},
        {2, -3, -6},
        {-2, -3, 6},
    }

    for i, tt := range tests {
        t.Run(fmt.Sprintf("Test case %d", i), func(t *testing.T) {
            result := Multiply(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Multiply(%d, %d) = %d, want %d", tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

3. 测试覆盖率

Go语言提供了内置的测试覆盖率工具,帮助你了解测试覆盖情况。

bash 复制代码
# 运行测试并生成覆盖率报告
go test -cover

# 生成详细的覆盖率报告
go test -coverprofile=coverage.out

# 查看覆盖率报告
go tool cover -html=coverage.out

模拟(Mocking)

在测试中,我们经常需要模拟外部依赖,以隔离被测试的代码。

1. 手动模拟

go 复制代码
// user_service.go
package service

import (
    "errors"
)

// UserRepository 定义用户存储接口
type UserRepository interface {
    GetUserByID(id int) (string, error)
}

// UserService 定义用户服务
type UserService struct {
    repo UserRepository
}

// NewUserService 创建用户服务实例
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

// GetUserName 根据ID获取用户名
func (s *UserService) GetUserName(id int) (string, error) {
    if id <= 0 {
        return "", errors.New("invalid user ID")
    }
    return s.repo.GetUserByID(id)
}
go 复制代码
// user_service_test.go
package service

import (
    "errors"
    "testing"
)

// mockUserRepository 模拟UserRepository接口
type mockUserRepository struct {
    users map[int]string
    err   error
}

// GetUserByID 实现UserRepository接口
func (m *mockUserRepository) GetUserByID(id int) (string, error) {
    if m.err != nil {
        return "", m.err
    }
    if user, ok := m.users[id]; ok {
        return user, nil
    }
    return "", errors.New("user not found")
}

func TestGetUserName(t *testing.T) {
    tests := []struct {
        name     string
        id       int
        users    map[int]string
        err      error
        expected string
        wantErr  bool
    }{
        {
            name:     "valid user",
            id:       1,
            users:    map[int]string{1: "Alice"},
            err:      nil,
            expected: "Alice",
            wantErr:  false,
        },
        {
            name:     "invalid ID",
            id:       -1,
            users:    nil,
            err:      nil,
            expected: "",
            wantErr:  true,
        },
        {
            name:     "user not found",
            id:       999,
            users:    map[int]string{1: "Alice"},
            err:      nil,
            expected: "",
            wantErr:  true,
        },
        {
            name:     "repository error",
            id:       1,
            users:    nil,
            err:      errors.New("database error"),
            expected: "",
            wantErr:  true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            mockRepo := &mockUserRepository{
                users: tt.users,
                err:   tt.err,
            }
            service := NewUserService(mockRepo)

            result, err := service.GetUserName(tt.id)
            if (err != nil) != tt.wantErr {
                t.Errorf("GetUserName() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if result != tt.expected {
                t.Errorf("GetUserName() = %v, want %v", result, tt.expected)
            }
        })
    }
}

2. 使用模拟库

我们可以使用第三方库如mockery来自动生成模拟代码。

bash 复制代码
# 安装mockery
go install github.com/vektra/mockery/v2@latest

# 生成模拟代码
mockery --name=UserRepository

集成测试

集成测试用于测试多个组件之间的交互,确保系统作为一个整体正常工作。

1. 数据库集成测试

go 复制代码
import (
    "database/sql"
    "fmt"
    "testing"
    "time"
    _ "github.com/go-sql-driver/mysql"
)

func TestDatabaseIntegration(t *testing.T) {
    // 连接测试数据库
    db, err := sql.Open("mysql", "test:test@tcp(localhost:3306)/test_db")
    if err != nil {
        t.Fatalf("Failed to connect to database: %v", err)
    }
    defer db.Close()

    // 测试数据库连接
    if err := db.Ping(); err != nil {
        t.Fatalf("Failed to ping database: %v", err)
    }

    // 创建测试表
    _, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (
        id INT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(255) NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )`)
    if err != nil {
        t.Fatalf("Failed to create table: %v", err)
    }

    // 插入测试数据
相关推荐
一条小小yu4 小时前
Spring IoC的理解
java·后端·spring
茉莉玫瑰花茶5 小时前
GO [ 方法 ]
开发语言·后端·golang
Wx-bishekaifayuan5 小时前
django个性化旅游路线推荐平台49005-计算机课程设计、毕业设计
spring boot·后端·python·django·课程设计·express·旅游
Eric_见嘉5 小时前
在职前端 Skill 和 MCP 分享
前端·后端·agent
孟健7 小时前
出海开发者资金合规:从港卡结汇到完税申报实操
后端·架构
乌暮7 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习
架构技术专栏8 小时前
Skill 有效性与稳定性评估:用对照、重复与证据判断价值
后端
拔剑纵狂歌8 小时前
ClickHouse system.query_log 字段详解与排障实践
后端·clickhouse·1024程序员节
再吃一根胡萝卜8 小时前
把本地 RAG 问答做"准":多轮指代消解 + 云端语义向量 + TXT 章节切分
后端
怕浪猫9 小时前
2026年9月面18个后端
后端·面试·github