Skip to content

测试与性能基准

Phase 07 — Engineering · 知识点 05–07:Unit Test · Integration Test · Benchmark


1. 学习目标

完成本知识点后,你应该能够:

  • 编写 Unit Test 覆盖 service 层和纯函数逻辑
  • 使用 table-driven tests 组织多组用例
  • 编写 Integration Test 验证 HTTP API 与数据库交互
  • 使用 httptest 测试 Handler 而无需启动真实端口
  • 编写 Benchmark 评估 JSON 序列化、Hub 广播等热点路径
  • 运行 go test -race 检测 WebSocket Hub 竞态条件

2. 为什么需要

数字孪生后端涉及 AGV 坐标校验、告警规则、Room 路由——逻辑错误会导致 Three.js 显示错误或漏报告警。没有测试,每次重构 WebSocket 或 Pipeline 都是赌博。

Go 内置测试框架,无需引入 Jest 级重型依赖。Unit Test 保证逻辑正确,Integration Test 保证组件协作,Benchmark 在优化 Concurrent Connections 时有数据支撑。


3. 核心概念

3.1 Unit Test(单元测试)

  • 测试单个函数/方法,依赖用 Mock 或 Fake 替代
  • 文件命名:xxx_test.go,函数:func TestXxx(t *testing.T)
  • 快、稳定、无外部网络/DB(理想情况)

3.2 Integration Test(集成测试)

  • 测试多个组件协作:HTTP → service → PostgreSQL
  • 可用 Docker 启动测试 DB,或 testcontainers-go
  • 本路线可先用内存 SQLite / 内嵌 Postgres 简化

3.3 Benchmark(基准测试)

  • 函数:func BenchmarkXxx(b *testing.B)
  • 测量 ns/op、内存分配
  • 用于对比优化前后 JSON、Hub 广播性能

3.4 测试命令

bash
go test ./...                    # 所有包
go test -v ./internal/service/   # 详细输出
go test -race ./...              # 竞态检测
go test -bench=. ./internal/ws/  # 基准测试
go test -cover ./...             # 覆盖率

4. 基础语法

4.1 Table-driven Unit Test

go
// internal/service/alert_test.go
package service

import "testing"

func TestLowBatteryAlert(t *testing.T) {
    tests := []struct {
        name    string
        battery int
        want    bool
    }{
        {"normal", 50, false},
        {"low", 15, true},
        {"boundary", 20, false},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := ShouldAlertLowBattery(tt.battery, 20)
            if got != tt.want {
                t.Errorf("battery=%d: got %v, want %v", tt.battery, got, tt.want)
            }
        })
    }
}

4.2 httptest Handler 测试

go
func TestGetDeviceSnapshot(t *testing.T) {
    svc := service.NewDeviceService(fakeRepo{})
    mux := http.NewServeMux()
    handler.RegisterDeviceRoutes(mux, svc)

    req := httptest.NewRequest(http.MethodGet, "/api/v1/devices/AGV-001", nil)
    rec := httptest.NewRecorder()
    mux.ServeHTTP(rec, req)

    if rec.Code != http.StatusOK {
        t.Fatalf("status: got %d", rec.Code)
    }
    var body map[string]any
    if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
        t.Fatal(err)
    }
}

4.3 Fake Repository

go
type fakeRepo struct{}

func (f fakeRepo) GetByID(ctx context.Context, id string) (*domain.Device, error) {
    return &domain.Device{ID: id, X: 10, Y: 20}, nil
}

4.4 Benchmark

go
func BenchmarkBroadcast(b *testing.B) {
    hub := ws.NewHub()
    go hub.Run()
    // 注册 N 个 mock clients...
    payload := []byte(`{"type":"agv.position","deviceId":"AGV-001"}`)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        hub.Broadcast(payload)
    }
}

4.5 Race 检测

WebSocket Hub 的 register/unregister/broadcast 必须在 -race 下无报警:

bash
go test -race ./internal/ws/ -count=1

5. 代码解析

t.Run 子测试:失败时定位到具体 case 名称,类似 Jest 的 it.each

httptest.NewRecorder:内存 ResponseWriter,不占用端口,测试并行安全。

b.ResetTimer():排除 setup 时间,只计循环体。

Fake vs Mock:Fake 是简化实现;Mock 通常指 testify/mock 等代码生成,本路线 Fake 足够。


6. JavaScript / TypeScript 对比

概念Jest / VitestGo testing
文件*.test.ts*_test.go
断言expect(x).toBe(y)if got != want { t.Error(...) }
表驱动test.each[]struct{...} + t.Run
HTTP 测试supertesthttptest
覆盖率--coverage-cover
性能无内置-bench 内置

Go 测试与源码同包或 _test 外部测试包(package service_test 测公开 API)。


7. 常见错误

错误 1:测试依赖真实生产 DB

测试应可重复、隔离,使用内存实现或测试容器。

错误 2:不测边界条件

电量 20%、坐标 0、空 room 等边界是 bug 温床。

错误 3:忽略 -race

Hub 偶发 panic 难以复现,race detector 能提前发现。

错误 4:Benchmark 在 CI 中不设 -benchtime

默认 1s 可能波动大,关注趋势而非绝对值。

错误 5:Integration Test 与 Unit Test 混在同一文件无 build tag

可用 //go:build integration 分离慢测试。


8. 实际应用

Project 03 测试清单

层级测试内容
service低电量告警、坐标边界、Room 路由逻辑
handlerGET snapshot 200/404,非法 JSON 400
wsHub 注册/注销、Broadcast 不 panic、race 通过
benchmarkjson.Marshal(AGVPositionMsg)、Broadcast 100 clients

CI 最小命令(Phase 08 详述):

bash
go vet ./...
go test ./...
go test -race ./...

9. 深入理解

9.1 外部测试包

package handler_test 只能测导出符号,模拟真实调用方视角。

9.2 TestMain

go
func TestMain(m *testing.M) {
    setup()
    code := m.Run()
    teardown()
    os.Exit(code)
}

Integration Test 共享 DB setup/teardown。

9.3 Fuzzing(Go 1.18+)

go
func FuzzParseCoordinate(f *testing.F) {
    f.Add("12.5")
    f.Fuzz(func(t *testing.T, s string) { ... })
}

可选扩展,用于解析客户端消息的健壮性。


10. 练习

详细练习见 exercises/phase-07-engineering/02-testing-benchmark.md

Level 1 — 基础

练习 1.1:为 ShouldAlertLowBattery 写 table-driven test。

练习 1.2:用 httptest 测试 GET /health 返回 200。

Level 2 — 应用

练习 2.1:Fake Repository + service 层 GetDevice 测试。

练习 2.2:Hub register/unregister 后 clients 数量断言。

Level 3 — 综合

练习 3.1:Integration test:POST 创建设备 → GET 验证。

练习 3.2go test -race ./internal/ws/ 修复竞态。

Level 4 — 项目实践

练习 4.1:Project 03 核心路径测试覆盖率 > 60%,benchmark 报告 JSON 序列化 ns/op。


11. 学习检查

  1. *_test.go 文件如何与源码关联?
  2. table-driven test 的优势是什么?
  3. httptest 与真实 ListenAndServe 测试有何不同?
  4. 何时用 Integration Test 而非 Unit Test?
  5. -race 解决什么问题?

12. 下一步

已完成下一知识点关系
Unit · Integration · BenchmarkDocker · Docker Compose测试通过 → 容器化交付

下一步学习 Docker 镜像构建与 Compose 多服务编排,实现本地一键启动 Go + PostgreSQL + Redis。


学习导航

上一篇:项目架构与工程基础 · 对应练习 · 下一篇:Docker 与 Docker Compose