Posted in

Go test命令不会写?从go test -v到testify/assert/benchmark全覆盖,附12个可复用测试模板

第一章:Go测试生态概览与test命令核心价值

Go 语言将测试能力深度内置于工具链中,go test 不是第三方插件,而是与 go buildgo run 并列的一等公民命令。它原生支持单元测试、基准测试、模糊测试(Go 1.18+)和示例测试,无需额外依赖即可完成从编写、执行到覆盖率分析的完整闭环。

测试文件约定与组织规范

Go 要求测试代码必须存放在以 _test.go 结尾的文件中,且包名通常为 xxx_test(与被测包同名加 _test 后缀)。例如,mathutil.go 的测试应位于 mathutil_test.go 中,包声明为 package mathutil_test。这种命名与包结构约定使 go test 能自动识别并隔离测试上下文,避免污染主构建环境。

go test 命令的核心能力

go test 默认仅运行 Test* 函数(函数名以 Test 开头,参数为 *testing.T),但可通过标志扩展行为:

  • go test -v:显示每个测试函数的名称与日志输出;
  • go test -run="^TestAdd$" -v:正则匹配精确执行单个测试;
  • go test -bench=.:运行所有 Benchmark* 函数;
  • go test -coverprofile=coverage.out && go tool cover -html=coverage.out:生成并可视化覆盖率报告。

测试驱动开发的最小可行流程

# 1. 创建被测函数(calculator.go)
func Add(a, b int) int { return a + b }

# 2. 编写对应测试(calculator_test.go)
func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5
    if got != want {
        t.Errorf("Add(2,3) = %d, want %d", got, want) // 失败时输出清晰差异
    }
}

# 3. 执行验证
go test -v ./...  # 递归运行当前模块下所有测试
特性 说明 是否需额外配置
并行测试 使用 t.Parallel() 标记后自动并发执行
子测试 t.Run("case1", func(t *testing.T){...}) 实现嵌套分组
测试超时 go test -timeout=30s 全局控制

go test 的简洁性与确定性,使其成为 Go 工程实践中可信赖的“事实标准”——它不鼓励魔法,只提供可预测、可组合、可脚本化的测试原语。

第二章:go test基础命令详解与实战演练

2.1 go test基本语法与执行机制解析

Go 测试由 go test 命令驱动,自动发现并执行以 _test.go 结尾的文件中符合 func TestXxx(*testing.T) 签名的函数。

执行入口与约定

  • 测试文件必须置于同一包内(package xxx),且文件名含 _test
  • 函数名需以 Test 开头,首字母大写的驼峰命名(如 TestHTTPTimeout
  • *testing.T 参数提供失败断言(t.Fatal)、日志(t.Log)和子测试控制能力

基础命令示例

go test                    # 运行当前包所有测试
go test -v                 # 显示详细输出(含 `t.Log`)
go test -run=^TestLogin$   # 精确匹配测试函数名

-run 支持正则匹配,^TestLogin$ 确保仅执行 TestLogin,避免误触 TestLoginWithOAuth

核心参数对照表

参数 作用 典型场景
-count=1 强制单次运行(禁用缓存) 排查竞态或状态残留
-short 跳过耗时测试(需 if testing.Short() 判断) CI 快速反馈
-bench=. 运行所有基准测试 性能回归分析
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("expected 5, got %d", result) // t.Error 不终止,t.Fatal 终止当前测试
    }
}

该测试验证 Add 函数逻辑;t.Errorf 输出错误信息并继续执行同函数内后续断言,适合多条件校验场景。

2.2 -v、-run、-failfast等常用标志的调试实践

Go 测试框架提供轻量但强大的命令行标志,精准控制测试执行行为。

详尽输出:-v 启用详细模式

go test -v ./...  # 显示每个测试函数名、执行时间及日志输出

-v 激活 verbose 模式,使 t.Log()t.Logf() 输出可见,并在测试开始/结束时打印函数签名与耗时,便于定位挂起或慢速测试。

精准靶向:-run 正则匹配

go test -run ^TestUserValidation$  # 仅运行完全匹配的测试
go test -run User.*Email            # 匹配含 "User" 后接 "Email" 的测试名

-run 接受 Go 正则语法,支持快速聚焦单个测试或子集,避免全量执行开销。

故障止损:-failfast 中断模式

标志 行为 适用场景
-failfast 首个测试失败即终止 CI 快速反馈、本地验证修复有效性
(默认) 继续运行全部测试 覆盖率统计、批量问题发现
graph TD
    A[go test] --> B{-failfast?}
    B -->|是| C[运行测试直至首次失败]
    B -->|否| D[执行全部测试用例]

2.3 测试文件命名规范与包级测试组织策略

命名约定:清晰表达意图

Go 语言要求测试文件以 _test.go 结尾,且必须与被测包同名(非 main 包):

// user_service_test.go —— 正确:对应 user_service.go 所在包
// user_test.go —— 错误:易与 user.go 的单元测试混淆

逻辑分析:_test.go 后缀触发 go test 自动识别;文件名前缀应映射核心功能模块(如 auth, payment),避免泛化词(如 util_test.go 应细化为 jwt_validator_test.go)。

包级测试组织原则

  • 单一职责:每个测试文件聚焦一个业务子域
  • 分层隔离:internal/testdata/ 存放共享 fixture,禁止跨包引用
  • 并行安全:所有 t.Parallel() 测试需独占资源(如内存 DB 实例)

推荐结构对照表

组件类型 文件命名示例 说明
功能单元测试 order_processor_test.go 覆盖 OrderProcessor 核心逻辑
集成测试 order_repository_it_test.go _it 后缀,依赖真实 DB
模拟测试 payment_gateway_mock_test.go 显式标注 mock,使用 interface stub
graph TD
    A[order_service.go] --> B[order_service_test.go]
    A --> C[order_repository_it_test.go]
    C --> D[(PostgreSQL)]

2.4 测试覆盖率分析(-cover)与可视化落地

Go 原生 go test -cover 提供基础覆盖率统计,但需结合 -coverprofile 生成可解析的 coverage.out 文件:

go test -coverprofile=coverage.out -covermode=count ./...

-covermode=count 记录每行执行次数,支持热点路径识别;-coverprofile 指定输出路径,为后续可视化提供数据源。

生成报告并启动本地服务:

go tool cover -html=coverage.out -o coverage.html

go tool cover 将二进制 profile 转为交互式 HTML,支持逐文件/逐行高亮,红色未覆盖、绿色已覆盖。

常用覆盖率模式对比:

模式 含义 适用场景
set 是否执行过(布尔) 快速验证覆盖广度
count 执行次数(整型) 性能瓶颈分析
atomic 并发安全计数(推荐CI) 多 goroutine 测试

可视化落地依赖标准化流程:

  • ✅ 生成 profile → ✅ 转换 HTML → ✅ 集成 CI/CD 环节自动归档
  • ❌ 仅依赖终端数字(如 coverage: 72.3%)无法定位薄弱模块
graph TD
    A[go test -coverprofile] --> B[coverage.out]
    B --> C[go tool cover -html]
    C --> D[coverage.html]
    D --> E[浏览器交互式分析]

2.5 并行测试(t.Parallel)与资源竞争规避实操

Go 测试中启用 t.Parallel() 可显著缩短执行时间,但共享状态易引发竞态。核心原则:并行测试间禁止共享可变全局资源

数据同步机制

使用 sync.Mutexsync.Once 保护临界区:

var mu sync.Mutex
var initialized bool

func TestCacheInit(t *testing.T) {
    t.Parallel()
    mu.Lock()
    if !initialized {
        initCache() // 耗时初始化
        initialized = true
    }
    mu.Unlock()
    // 后续读操作无需锁
}

t.Parallel() 告知测试框架该测试可与其他并行测试并发执行;mu.Lock() 确保仅首个进入的 goroutine 执行初始化,避免重复或竞态。

常见避坑策略

  • ✅ 使用 t.TempDir() 创建隔离临时目录
  • ❌ 避免直接写入固定路径如 /tmp/test.db
  • ✅ 用 sync.Pool 复用非共享对象实例
方案 适用场景 安全性
t.TempDir() 文件/数据库临时存储 ✅ 高
sync.Map 并发读多写少的缓存 ✅ 中高
全局变量 + Mutex 必须共享的初始化状态 ⚠️ 需谨慎
graph TD
    A[启动并行测试] --> B{是否访问共享资源?}
    B -->|是| C[加锁/隔离/延迟初始化]
    B -->|否| D[直接执行,无同步开销]
    C --> E[释放锁/完成初始化]

第三章:断言增强与测试可读性提升

3.1 testify/assert核心断言方法对比与选型指南

断言语义差异解析

Equal() 比较值相等(调用 reflect.DeepEqual),而 Same() 要求指针相同(==):

a := []int{1, 2}
b := []int{1, 2}
assert.Equal(t, a, b) // ✅ 通过  
assert.Same(t, a, b)  // ❌ 失败:底层数组地址不同

Equal() 适用于业务逻辑校验;Same() 仅用于验证对象同一性(如单例、缓存命中)。

常见断言方法对比

方法 类型安全 深度比较 错误信息可读性 典型场景
Equal() DTO/结构体内容验证
EqualValues() 跨类型数值比较(int vs int64)
True() 布尔断言

性能与可维护性权衡

// 推荐:明确意图 + 可调试
assert.Len(t, users, 3, "expected exactly 3 active users")
// 不推荐:隐式转换易掩盖逻辑缺陷
assert.Equal(t, len(users), 3)

Len() 直接暴露被测对象,失败时打印完整切片内容,便于快速定位。

3.2 自定义错误消息与上下文追踪的工程化实践

错误上下文注入机制

在 HTTP 中间件中动态注入请求 ID 与业务标签,确保错误日志可追溯:

func WithContextTrace(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := context.WithValue(r.Context(), "req_id", uuid.New().String())
        ctx = context.WithValue(ctx, "service", "order-api")
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

逻辑分析:通过 context.WithValue 将唯一 req_id 和服务标识注入请求上下文;后续错误构造时可安全提取,避免全局变量污染。参数 r.WithContext(ctx) 确保链路透传。

错误结构标准化

统一错误类型支持消息、码、上下文字段:

字段 类型 说明
Code int 业务错误码(如 4001)
Message string 用户友好提示
DebugInfo map[string]interface{} 请求ID、堆栈、参数快照

上下文增强型错误构造

func NewAppError(code int, msg string, ctx context.Context) error {
    reqID := ctx.Value("req_id").(string)
    svc := ctx.Value("service").(string)
    return fmt.Errorf("[%s|%s] %s (code=%d)", reqID, svc, msg, code)
}

该函数将上下文元数据前置拼入错误字符串,便于 ELK 日志聚合与快速过滤。

3.3 表格驱动测试(Table-Driven Tests)标准化模板

表格驱动测试通过将输入、预期输出与测试元数据解耦,显著提升测试可维护性与覆盖密度。

核心结构要素

  • 测试用例集合:[]struct{input, want, name string}
  • 统一执行框架:for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ... }) }
  • 前置/后置钩子:如 setup() / teardown() 调用

标准化代码模板

func TestParseDuration(t *testing.T) {
    tests := []struct {
        name     string
        input    string
        want     time.Duration
        wantErr  bool
    }{
        {"zero", "0s", 0, false},
        {"invalid", "1y", 0, true},
    }
    for _, tt := range tests {
        tt := tt // capture range variable
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParseDuration(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("ParseDuration() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !tt.wantErr && got != tt.want {
                t.Errorf("ParseDuration() = %v, want %v", got, tt.want)
            }
        })
    }
}

逻辑分析tt := tt 防止闭包中变量复用;t.Run() 实现用例命名隔离;if (err != nil) != tt.wantErr 精确校验错误存在性而非内容。参数 name 支持快速定位失败项,wantErr 显式声明错误期望,避免隐式判空陷阱。

典型测试维度对照表

维度 推荐值示例 说明
name "2h30m_valid" 清晰反映输入语义与场景
input "2h30m" 实际传入函数的原始参数
want 2*time.Hour + 30*time.Minute 类型安全、可读性强的期望值
setup/teardown 按需嵌入 defer cleanup() 控制测试副作用边界

第四章:性能基准与高级测试场景覆盖

4.1 benchmark基础语法与-benchmem内存分析实战

Go 的 go test -bench 是性能基准测试核心机制,-benchmem 则启用内存分配统计。

基础语法结构

go test -bench=^BenchmarkRead$ -benchmem -benchtime=5s ./...
  • ^BenchmarkRead$:正则匹配函数名,确保精确执行
  • -benchmem:记录每次运行的 allocs/op(分配次数)和 bytes/op(字节数)
  • -benchtime=5s:持续运行至少 5 秒以提升统计置信度

内存分析实战示例

func BenchmarkCopySlice(b *testing.B) {
    src := make([]int, 1000)
    for i := range src {
        src[i] = i
    }
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = append([]int(nil), src...) // 触发新底层数组分配
    }
}

该基准测试每轮复制切片,-benchmem 将揭示每次 append 引发的堆分配行为。配合 go tool pprof 可进一步定位逃逸点。

指标 含义
allocs/op 每次操作的内存分配次数
bytes/op 每次操作的平均分配字节数

启用 -benchmem 后,输出中将明确显示内存开销,是识别隐式分配的关键开关。

4.2 基准测试结果解读与性能回归监控方案

数据同步机制

采用 Prometheus + Grafana 实现指标自动采集与阈值告警,关键延迟指标每15秒上报一次。

回归检测逻辑

# 检查最近3次基准运行中P95延迟是否连续上升 >8%
def is_regression(series: list[float]) -> bool:
    return all(series[i] > series[i-1] * 1.08 for i in range(1, 3))

series 为按时间排序的 P95 延迟数组(单位 ms);1.08 对应 8% 容忍波动,避免毛刺误判。

监控看板核心指标

指标名 阈值 采集频率 异常响应
req_latency_p95 >120ms 15s 自动触发 diff 分析
throughput_qps 1min 启动降级预案

流程闭环

graph TD
    A[定时执行基准] --> B[提取关键指标]
    B --> C{是否触发回归?}
    C -->|是| D[生成diff报告+Git提交比对]
    C -->|否| E[归档至时序库]

4.3 子测试(t.Run)构建嵌套测试树与用例隔离

Go 的 t.Run 是测试组织范式的转折点——它让单个测试函数可分解为逻辑内聚、生命周期独立的子测试节点。

为什么需要子测试?

  • 隔离失败:一个子测试 panic 不影响其余执行
  • 精准定位:失败时输出完整路径(如 TestValidate/valid_email
  • 共享 setup/teardown,避免重复代码

基础用法示例

func TestValidate(t *testing.T) {
    tests := []struct {
        name     string
        input    string
        wantErr  bool
    }{
        {"empty", "", true},
        {"valid", "a@b.c", false},
    }
    for _, tt := range tests {
        tt := tt // 闭包捕获
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateEmail(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateEmail() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

逻辑分析t.Run 接收子测试名与闭包函数;每个子测试拥有独立的 *testing.T 实例,支持 t.Fatalt.Skip 等操作而不污染兄弟节点。tt := tt 是必需的循环变量捕获,防止所有子测试共用最后一次迭代值。

子测试层级能力对比

特性 传统并列测试函数 t.Run 子测试
嵌套结构 ❌ 扁平命名模拟 ✅ 原生支持 TestX/Y/Z 树形路径
资源复用 依赖全局 setup ✅ 外层 test 函数天然共享 setup
graph TD
    A[TestValidate] --> B[empty]
    A --> C[valid]
    A --> D[invalid_domain]
    B --> B1[validate_format]
    C --> C1[validate_domain]

4.4 模拟依赖(mock)与测试辅助函数封装范式

为何需要封装 mock 行为?

直接在每个测试用例中重复 jest.mock()MockedClass.mockImplementation() 易导致冗余、耦合与维护困难。封装可提升可读性、复用性与一致性。

推荐的封装范式

  • 将 mock 初始化、预设返回值、清理逻辑统一收口
  • 每个辅助函数专注单一依赖(如 mockApiClient()mockDatabase()
  • 支持参数化行为(如 mockApiClient({ status: 404 })

示例:封装 HTTP 客户端 mock

// test-helpers/mockApiClient.ts
export const mockApiClient = (opts: { data?: any; status?: number } = {}) => {
  const { data = { id: 1 }, status = 200 } = opts;
  jest.mock('@/api/client', () => ({
    default: jest.fn().mockResolvedValue({ data, status }),
  }));
};

该函数动态覆盖 @/api/client 模块,默认返回成功响应;datastatus 参数支持按需定制异常路径,避免测试间污染。

封装后调用对比

场景 原始写法 封装后调用
正常响应 手动 mock + 3 行实现 mockApiClient()
错误响应 重复 mockImplementationOnce mockApiClient({status:500})
graph TD
  A[测试用例] --> B[调用 mockApiClient]
  B --> C[自动注入 mock 模块]
  C --> D[执行时返回预设响应]
  D --> E[断言业务逻辑]

第五章:12个开箱即用的Go测试模板总结

单元测试基础结构(无依赖)

适用于纯函数或无外部调用的逻辑模块。使用 t.Run 组织子测试,覆盖边界值与典型输入:

func TestCalculateTotal(t *testing.T) {
    tests := []struct {
        name     string
        items    []Item
        want     float64
        wantErr  bool
    }{
        {"empty slice", []Item{}, 0.0, false},
        {"single item", []Item{{Name: "book", Price: 19.99}}, 19.99, false},
        {"negative price", []Item{{Price: -5.0}}, 0.0, true},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := CalculateTotal(tt.items)
            if (err != nil) != tt.wantErr {
                t.Errorf("CalculateTotal() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !float64Equal(got, tt.want) {
                t.Errorf("CalculateTotal() = %v, want %v", got, tt.want)
            }
        })
    }
}

HTTP Handler 测试(net/http/httptest)

无需启动真实服务器,直接验证路由、状态码与响应体:

场景 方法 路径 预期状态码 验证点
成功获取用户 GET /api/users/123 200 JSON 结构 & id 字段
无效ID格式 GET /api/users/abc 400 错误消息含 "invalid ID"
用户不存在 GET /api/users/999999 404 响应体含 "not found"

并发安全验证模板

使用 sync.WaitGroup + t.Parallel() 模拟高并发写入,配合 -race 检测竞态:

func TestCounter_IncrementConcurrent(t *testing.T) {
    c := &Counter{}
    var wg sync.WaitGroup
    const N = 1000
    for i := 0; i < N; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Increment()
        }()
    }
    wg.Wait()
    if c.Value() != N {
        t.Errorf("expected %d, got %d", N, c.Value())
    }
}

数据库集成测试(SQLite 内存模式)

利用 sqlmock 或 SQLite :memory: 实现零外部依赖的 DB 测试:

func TestUserRepository_Create(t *testing.T) {
    db, mock, err := sqlmock.New()
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()

    mock.ExpectExec(`INSERT INTO users`).WithArgs("alice", "alice@example.com").WillReturnResult(sqlmock.NewResult(1, 1))

    repo := NewUserRepository(db)
    user := User{Username: "alice", Email: "alice@example.com"}
    err = repo.Create(context.Background(), &user)
    if err != nil {
        t.Fatal(err)
    }
    if assert.NoError(t, mock.ExpectationsWereMet()) {
        // mock 已按预期被调用
    }
}

接口实现一致性测试

确保所有实现了 DataProcessor 接口的类型均通过同一组契约测试:

func TestProcessor_Contract(t *testing.T) {
    processors := []DataProcessor{
        &JSONProcessor{},
        &XMLProcessor{},
        &CSVProcessor{},
    }
    for _, p := range processors {
        t.Run(fmt.Sprintf("%T", p), func(t *testing.T) {
            input := []byte(`{"valid":true}`)
            _, err := p.Process(input)
            if err != nil {
                t.Fatalf("failed to process valid input: %v", err)
            }
        })
    }
}

定时任务行为验证(time.Now 替换)

通过注入 clock.Clock 接口控制时间流,精确断言延时逻辑:

func TestScheduler_NextRunAt(t *testing.T) {
    fixedClock := clock.NewMock()
    sched := NewScheduler(fixedClock)

    // 设置当前时间为 2024-01-01T10:00:00Z
    fixedClock.Set(time.Date(2024, 1, 1, 10, 0, 0, 0, time.UTC))
    next := sched.NextRunAt("0 0 * * *") // 每日零点
    expected := time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)
    if !next.Equal(expected) {
        t.Errorf("NextRunAt() = %v, want %v", next, expected)
    }
}

错误分类断言模板

区分 errors.Is(底层错误)与 errors.As(错误类型),避免字符串匹配脆弱性:

func TestService_FetchWithRetry(t *testing.T) {
    client := &mockHTTPClient{failCount: 2, statusCode: 503}
    svc := NewService(client)

    _, err := svc.Fetch(context.Background(), "https://api.example.com/data")
    if !errors.Is(err, ErrServiceUnavailable) {
        t.Error("expected ErrServiceUnavailable, got different error")
    }
    var netErr *url.Error
    if !errors.As(err, &netErr) {
        t.Error("expected *url.Error in error chain")
    }
}

配置加载验证(Viper + YAML)

使用嵌入式 YAML 字符串测试配置解析逻辑,覆盖缺失字段、类型错误、默认值回退:

func TestLoadConfig_FromYAML(t *testing.T) {
    yamlContent := `
server:
  port: 8080
  timeout: 30s
database:
  url: "sqlite://:memory:"
`
    v := viper.New()
    v.SetConfigType("yaml")
    _ = v.ReadConfig(strings.NewReader(yamlContent))

    cfg := Config{}
    err := v.Unmarshal(&cfg)
    if err != nil {
        t.Fatal(err)
    }
    if cfg.Server.Port != 8080 {
        t.Errorf("expected port 8080, got %d", cfg.Server.Port)
    }
}

CLI 命令执行测试(cobra + test helpers)

捕获 stdout/stderr 并验证命令输出与退出码:

func TestRootCmd_Execute(t *testing.T) {
    rootCmd := NewRootCommand()
    rootCmd.SetArgs([]string{"version"})
    rootCmd.SetOut(new(bytes.Buffer))
    rootCmd.SetErr(new(bytes.Buffer))

    err := rootCmd.Execute()
    if err != nil {
        t.Fatal(err)
    }
    // 断言 stdout 包含语义化版本号如 "v1.2.3"
}

文件系统操作隔离测试(afero)

替换 os 操作为内存文件系统,避免污染真实磁盘:

func TestFileWriter_WriteToFile(t *testing.T) {
    fs := afero.NewMemMapFs()
    writer := NewFileWriter(fs)

    err := writer.Write("config.json", []byte(`{"env":"test"}`))
    if err != nil {
        t.Fatal(err)
    }

    exists, _ := afero.Exists(fs, "config.json")
    if !exists {
        t.Error("expected file to exist in memory fs")
    }
}

gRPC 服务端测试(bufconn)

使用内存连接 bufconn.Listener 启动服务端,客户端直连,不依赖网络栈:

func TestUserService_GetUser(t *testing.T) {
    lis := bufconn.Listen(1024 * 1024)
    srv := grpc.NewServer()
    pb.RegisterUserServiceServer(srv, &userServiceMock{})

    go func() {
        if err := srv.Serve(lis); err != nil {
            log.Fatal(err)
        }
    }()

    conn, _ := grpc.DialContext(context.Background(), "bufnet",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithContextDialer(bufDialer(lis)))
    defer conn.Close()

    client := pb.NewUserServiceClient(conn)
    resp, err := client.GetUser(context.Background(), &pb.GetUserRequest{Id: "123"})
    if err != nil {
        t.Fatal(err)
    }
    if resp.User.Id != "123" {
        t.Error("unexpected user ID in response")
    }
}

日志输出结构化验证(testify/assert + zap)

捕获 zapcore.Core 输出,解析 JSON 日志条目并断言字段值:

func TestLogger_ErrorWithFields(t *testing.T) {
    core, logs := observer.New(zap.DebugLevel)
    logger := zap.New(core)

    logger.Error("db query failed",
        zap.String("query", "SELECT * FROM users"),
        zap.Int("attempts", 3),
        zap.Error(errors.New("timeout")))

    if logs.Len() != 1 {
        t.Fatalf("expected 1 log entry, got %d", logs.Len())
    }
    entry := logs.All()[0]
    if entry.Message != "db query failed" {
        t.Errorf("wrong message: %s", entry.Message)
    }
    if entry.Context[1].String != "timeout" {
        t.Error("error message not captured in structured log")
    }
}

专注后端开发日常,从 API 设计到性能调优,样样精通。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注