CAUTION
本文源于我在公司内部的一次技术分享,关键信息已做修改、脱敏处理
很多人跟我说,写单测纯纯浪费时间,功能写完,自测一下能跑就行,何必费那么大功夫呢?
恰恰相反,写单元测试是一种长期投资。你每次提交代码触发 CI 自动跑完一轮测试,你就能看到这份投资在持续产生“复利”。复利的力量有多强?爱因斯坦称它为“世界第八大奇迹”
当你未来修改代码时,一定会庆幸自己当初写下的那些单测。至于觉得浪费时间,多半是因为还没掌握正确的方法。有策略地写单测,才能让它真正帮你提升开发效率
业务开发中,没必要为所有代码写单测。优先覆盖核心逻辑、容易出错的部分,以及公共接口。写单测之前,先明确测试对象,是类、函数、库的 API,还是 Web 接口?单测的目的是确保模块行为符合预期。覆盖率固然重要,但不应盲目追求覆盖率,否则南辕北辙。代码修改后,单元测试不该被破坏。(除非需求调整)有些隐藏的问题,Code Review 不好人肉出来,只有通过测试才能暴露出来
本次分享内容:
其实,单元测试不仅是验证功能正确性的工具,更会潜移默化地教你如何写出高质量的代码。通常情况下,“垃圾代码”、“屎山”通常是不可测试的,有如下特点:
抽象前
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,从而实现独立测试
抽象外部依赖,设计无状态模块。将数据库、网络服务等外部依赖抽离,通过 DI 的方式传入。尽量让模块保持无状态(无副作用,即 Side Effect),这样测试时不依赖全局环境,易于验证
使用重构技术降低复杂度。利用卫语句、小函数、清晰的条件表达式、DSL、LoD、合理的编程范式等手段,使逻辑简单、可读、可维护
归根结底,能测试的代码更容易维护,而易维护的代码才算高质量。单元测试不仅能配合 CI 自动化检查功能,还能帮你在开发中不断优化代码
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 生成伪数据,用于测试不同场景
行为驱动测试,以场景为驱动,明确输入、行为、期望结果。以 Go 举例的话可以选择 GoConvey 框架,通常会提供更直观的 BDD 语法
使用断言库,替代 fmt.Println,让测试可自动化,方便 CI/CD 集成
衡量测试覆盖了多少代码路径。不要过于极端地追求覆盖率,重点覆盖核心逻辑和边界条件
避免副作用、保持代码无状态,是编写可测试代码的关键,这能让 Mock 更简单、断言更准确、覆盖率更具意义
案例代码,基于 Golang
fmt.Printlntime.Now()fmt.Println github.com/stretchr/testify/assertimport (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAdd(t *testing.T) {
result := Add(2, 3)
assert.Equal(t, 5, result, "2 + 3 应该等于 5")
}import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"both positive", 2, 3, 5},
{"positive + negative", 5, -2, 3},
{"both negative", -3, -7, -10},
{"zero", 0, 5, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if result := Add(tt.a, tt.b); result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
func Add(a, b int) int {
return a + b
}github.com/uber-go/mockmockgen -source=commentfilter.go -destination=mock_commentfilter.go -package=cfgithub.com/bouk/monkeyGOARCH=amd64,因为该库不支持 arm64 架构import "github.com/bouk/monkey"
// 示例: 替换函数实现
monkey.Patch(someFunc, func(a int) int { return 42 })使用 Copilot 生成测试模板,通过依赖注入让代码更易 Mock 与扩展,人工补充边界与异常场景,完善覆盖率与质量
| 案例 | 看点 | 代码传送门 |
|---|---|---|
| Minikube | 表驱动测试 | start_test.go |
| Nacos | 依赖注入 Mock | HealthControllerTest.java |
| vue-realworld-example-app | BDD 框架案例 | VPagination.spec.js |