概念
原则:
- 写单测之前,先弄明白要测试的对象是什么(类、函数、库的 API、还是 web 接口等)
- 业务开发,没有必要为所有代码写测试
不可测试的代码:
- 复杂的状态依赖
- 没做抽象:直接在方法内部创建依赖对象(数据库、网络服务等写死)-> 没有 DI,无法 Mock
- 副作用:操作全局变量,模块难以控制其状态 -> 不幂等,每次测试结果都不一样
- 嵌套太多:业务逻辑、控制逻辑 复杂度 -> 代码路径难以被覆盖
抽象和细节是啥?
抽象前 💩
public class List {
public void add(Object element) {
if (!readOnly) {
int newSize = size + 1;
if (newSize > elements.length) {
Object[] newElements = new Object[elements.length + 10];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
}
elements[size++] = element;
}
}
}
抽象后 💃
public class List {
public void add(Object element) {
if (readOnly) {
return;
}
if (atCapacity()) {
grow();
}
addElement(element);
}
private addElement(Object Element) {
elements[size++] = element;
}
}
写可测试的代码:
- 面向接口编程 -> 可 Mock
- 抽象外部依赖,设计无状态的模块 -> OOP 的依赖注入/DSL
- 使用重构技术,降低复杂度 -> 卫语句、小函数、DSL、可读的条件表达式、LoD、编程范式等
LoD 是啥?
LoD 前
func (builder *WelcomeReplyBuilder) BuildReply() (*Result, error) {
// 1、选择模版
tpl, err := selectTpl(redis.LiveWelcomeKey)
if err != nil {
return nil, err
}
// 2、解析模版
placeholders, formatTpl := resolveTpl(tpl)
// 3、昵称取前两个字
shortNickname := ""
nickname := builder.roomEvent.DyName
runeCount := utf8.RuneCountInString(nickname)
if runeCount <= 2 {
// 如果字符数量小于等于 2,直接返回原始字符串
shortNickname = nickname
} else {
firstTwoRunes := []rune(nickname)[:2]
shortNickname = string(firstTwoRunes)
}
// 4、应用模版 生成欢迎语
res := applyTpl(formatTpl, placeholders, shortNickname)
return &Result{Reply: res}, nil
}
LoD 后
func (builder *WelcomeReplyBuilder) BuildReply() (*Result, error) {
// 选择模版
tpl, err := selectTpl(redis.LiveWelcomeKey)
if err != nil {
return nil, err
}
// 解析模版
placeholders, formatTpl := resolveTpl(tpl)
// 昵称取前两个字
shortNickname := builder.roomEvent.GetShortNickname()
// 应用模版 生成欢迎语
res := applyTpl(formatTpl, placeholders, shortNickname)
return &Result{Reply: res}, nil
}
// room_event.go
func (e *RoomEvent) GetShortNickname() string {
nickname := e.DyName
// 获取字符串的 Unicode 字符数量
runeCount := utf8.RuneCountInString(nickname)
// 如果字符数量小于等于 2,直接返回原始字符串
if runeCount <= 2 {
return nickname
}
// 获取前两个字符的 Unicode 编码
firstTwoRunes := []rune(nickname)[:2]
// 将 Unicode 编码转换为字符串
shortName := string(firstTwoRunes)
return shortName
}
单元测试术语
- Mock、Stub、Spy、Faker -> 隔离外部依赖/状态
- BDD、GivenWhenThen 测试场景/Gherkin
a. 基于 Go 语言特性 t.Run+表驱动测试
b. BDD 框架(GoConvey) - 断言 -> 解决 “print 大法” 无法集成进 CICD,实现自动化测试
- 覆盖率
演示
案例背景:
直播间事件处理器
演示内容:
演示 1:断言库(assert)
github.com/stretchr/testify
演示 2:表驱动测试、覆盖率、mockgen 生成 Mock 实现
github.com/uber-go/mock
mockgen -source=commentfilter.go -destination=mock_commentfilter.go -package=cf
演示 3:新增测试用例,演示 Mock 打桩,继续完善覆盖率
演示 4:猴子补丁
在运行时动态修改代码,替换函数、方法或变量的实现,而不需要更改源代码
github.com/bouk/monkey
GOARCH=amd64
演示 5:使用 Copilot 生成 80%测试代码(依赖注入式的构造器)
社区实践: