💡 导读 :本文结合开源项目
easyms.golang,分享为核心模块补齐单元测试的完整实践,手把手教你用 Go 单元测试守护系统质量。
🚨 你是否也经历过这种"惊险时刻"?
明明只是修复一个小 Bug,却意外引发一系列连锁问题?
当系统日益复杂,每一次上线都如同在精密平衡中推进。
本文将结合开源项目 easyms.golang,分享一次为核心模块补齐单元测试的实践过程。
👋 开篇介绍
大家好,我是 Louis。
今天聊一个所有程序员都绕不开、但又常常被低估的话题:单元测试。
很多程序员觉得它多余,也有人认为是"多此一举"。我们不急于评判对错,但确定的是:需要时我们必须知道如何用好它。
从上篇文章反馈看,大家更喜欢故事形式聊技术,那今天我们就从一个真实经历说起。
📖 一、那次故障,让我重新认识测试的价值
说实话,在很长一段时间里,我对测试的态度和许多人一样:
知道重要,但项目赶、需求急时,总想着"以后再补"。
💥 故障回顾
在 easyms.golang 项目的一次迭代中,我只是想优化 TokenService 的一个小逻辑,改了几行代码:
- ✅ 本地运行正常
- ✅ CI 检查顺利通过
- ✅ 合并代码并部署上线
然而不久后,用户登录接口在部分边缘场景下开始出现异常报错。
🔍 排查结果 :那一行看似"无害"的改动,悄然改变了某个核心分支的行为------而这段代码,没有任何单元测试覆盖。
💭 深刻感悟
没有测试覆盖的核心模块,就像一座缺少地基的建筑,
你无法预判它何时会因微小变动而失去稳定。
从那以后,我下定决心:必须为核心业务建立系统性的单元测试。
🎯 二、真正的难点:高度耦合的代码,该如何测试?
这次我从 auth-svc 开始。UsernamePasswordTokenGranter 是登录流程的核心组件:
📝 核心代码结构
go
type UsernamePasswordTokenGranter struct {
userDetailsService UserDetailsService
tokenService TokenService
}
func (t *UsernamePasswordTokenGranter) Grant(...) (*models.OAuth2Token, error) {
userDetails, err := t.userDetailsService.LoadUserByUsername(...)
if !userDetails.CheckPassword(...) { ... }
return t.tokenService.CreateAccessToken(...)
}
依赖两个外部接口:
UserDetailsService:根据用户名加载用户信息TokenService:验证成功后生成访问令牌
问题:如何为这样高度依赖外部服务的代码编写单元测试?
🧠 三、单元测试的核心原则:隔离,而非"全流程复现"
❌ 常见误区
"那我是不是要连数据库?要不要起一套完整服务?"
这已经偏向集成测试范畴。
✅ 正确认知
单元测试的目标是:
仅验证当前函数的业务逻辑是否正确,而非验证整个系统能否运行。
我们应关注:
- 用户存在 / 不存在
- 密码正确 / 错误
- 各分支是否返回符合预期的结果
不关心:
- 用户数据来自哪里
- 是否是真实数据库记录
- Token 具体如何生成
🛠️ 四、解决方案:用 Mock 隔离依赖
要实现隔离,必须引入 Mock(模拟对象)。
🔧 Mock 实现示例
go
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type mockUserDetailsService struct {
mock.Mock
}
func (m *mockUserDetailsService) LoadUserByUsername(username string) (*model.UserDetails, error) {
args := m.Called(username)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.UserDetails), args.Error(1)
}
💡 Mock 本质:不执行真实逻辑,由我们预设其行为与返回值。
🎬 五、把测试当作"情景模拟"
我推荐用"情景模拟"视角理解单元测试------为不同情况编写预设脚本,逐一验证系统反应。
三步法:
- 准备(Arrange):初始化对象、预设 Mock 行为
- 执行(Act):调用被测方法
- 断言(Assert):验证输出与行为是否符合预期
🎯 场景一:登录成功(理想路径)
go
func TestUsernamePasswordTokenGranter_Grant_Success(t *testing.T) {
// 1. 准备
mockUserSvc := new(mockUserDetailsService)
mockTokenSvc := new(mockTokenService)
granter := NewUsernamePasswordTokenGranter("password", mockUserSvc, mockTokenSvc)
correctUser := &model.UserDetails{Username: "testuser", Password: "password"}
err := correctUser.HashPassword()
assert.NoError(t, err)
clientDetails := &model.ClientDetails{ClientId: "test-client"}
tokenRequest := &model.TokenRequest{Username: "testuser", Password: "password"}
// 预设 Mock 行为
mockUserSvc.On("LoadUserByUsername", "testuser").Return(correctUser, nil)
mockTokenSvc.On("CreateAccessToken", mock.Anything).Return(&model.OAuth2Token{}, nil)
// 2. 执行
token, err := granter.Grant(context.Background(), "password", clientDetails, tokenRequest)
// 3. 断言
assert.NoError(t, err)
assert.NotNil(t, token)
mockUserSvc.AssertExpectations(t)
mockTokenSvc.AssertExpectations(t)
}
✅ 验证要点:正确用户名密码 → 成功加载用户 → 正常生成 Token
🎯 场景二:用户不存在(异常路径)
go
func TestUsernamePasswordTokenGranter_Grant_UserNotFound(t *testing.T) {
mockUserSvc := new(mockUserDetailsService)
mockTokenSvc := new(mockTokenService)
granter := NewUsernamePasswordTokenGranter("password", mockUserSvc, mockTokenSvc)
clientDetails := &model.ClientDetails{ClientId: "test-client"}
tokenRequest := &model.TokenRequest{Username: "unknownuser", Password: "password"}
mockUserSvc.On("LoadUserByUsername", "unknownuser").Return(nil, errors.New("user not found"))
_, err := granter.Grant(context.Background(), "password", clientDetails, tokenRequest)
assert.Error(t, err)
mockTokenSvc.AssertNotCalled(t, "CreateAccessToken") // 失败时不应调用 TokenService
}
🔍 关键验证 :用户不存在时,TokenService 不应被调用。
📈 六、补齐测试后,我的三点深刻变化
1️⃣ 重构不再靠"胆子大",而是靠"底气足"
以前改代码凭经验;现在改代码先看测试是否通过。
测试通过,心中才有底。
2️⃣ 测试是设计的"镜子"
如果一个函数依赖过多、难以 Mock、测试写得异常艰难,往往说明它本身职责不清、耦合过高。
3️⃣ 测试是最好的活文档
相比容易过时的注释,测试用例清晰定义了每种输入应有的输出。
🎯 七、写在最后
写单元测试,短期内确实会增加工作量。它可能"老板看不见,客户摸不着"。
但从长远看,收益远超投入:
- ✅ 更稳定的系统表现
- ✅ 更安全的重构能力
- ✅ 更专业的工程素养
- ✅ 更早暴露隐患,减少线上风险
⚠️ 特别提醒 :建议优先覆盖核心业务流程,不必追求 100% 覆盖率。
📌 项目地址
GitHub :github.com/louis-xie-p...
Gitee :gitee.com/louis_xie/e...
💬 互动话题
欢迎在评论区分享:
- 你对单元测试的真实看法
- 你在测试中踩过的坑
- 你们团队的测试策略
点赞 + 在看 + 转发,让更多人重视单元测试的价值!