第一章:Go测试生态概览与test命令核心价值
Go 语言将测试能力深度内置于工具链中,go test 不是第三方插件,而是与 go build、go 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.Mutex 或 sync.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.Fatal、t.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模块,默认返回成功响应;data和status参数支持按需定制异常路径,避免测试间污染。
封装后调用对比
| 场景 | 原始写法 | 封装后调用 |
|---|---|---|
| 正常响应 | 手动 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")
}
} 