Go 测试中 MustCreate 模式的实用技巧
来源标题:The MustCreate Pattern for Go Tests
作者分享了一种让 Go 测试代码更易读、更少重复的 MustCreate 模式。该模式通过编写 MustCreateUsername 等辅助函数包装可能失败的构造函数,省去测试中冗长的错误检查代码。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
我最近发现了一个巧妙的方法,能让 Go 测试更具可读性,同时减少重复代码。
假设我有一个可能失败的构造函数:
func NewUsername(raw string) (Username, error) { ... }
我还有一个函数,它接收一个 Username 作为参数:
func (ds Datastore) ReadUser(username Username) (User, error) { ... }
在给 ReadUser 写测试时,我会用一个确定有效的值调用 NewUsername,但仍然不得不写一大堆冗长的错误检查代码,才能让编译器相信一切正常:
username, err := NewUsername("joe123") // 这很傻,因为我知道 joe123 是合法的用户名,所以这里 // 根本不应该出错。 if err != nil { t.Fatalf("failed to create username: %v", err) // 执行我真正关心的测试断言。 user, err := store.ReadUser(username) if got, want := user.Name.String(), "joe123"; got != want { t.Errorf("username=%v, want=%v", got, want)
我过去的做法:为每个类型写一个辅助函数 🔗︎
直到几个月前,我一直通过编写辅助函数来避免冗长的错误检查,比如写一个 MustCreateUsername,它包装 NewUsername,并假定永远不会返回非 nil 的 error:
user, err := store.ReadUser(MustCreateUsername(t, "joe123")) if got, want := user.Name.String(), "joe123"; got != want { t.Errorf("username=%v, want=%v", got, want)
MustCreateUsername 的实现和你预期的一样:
func MustCreateUsername(t testing.TB, raw string) Username { t.Helper() username, err := NewUsername(raw) if err != nil { t.Fatalf("invalid username: %s", raw)
辅助函数策略本身运行良好,但扩展性很差。最终,我的仓库里会堆满 MustCreateFoo 和 MustCreateBar,为应用中的每个类型各写一个。
2022 年,Go 1.18 加入了对泛型的支持。我喜欢泛型这个想法,但很少遇到必须用泛型才能解决的问题。
最近我意识到,泛型才是解决 MustCreateX 泛滥问题的更好办法。与其为每个类型定义一个辅助函数 MustCreateX,不如定义一个通用的 MustCreate 测试辅助函数,这样我的测试就变成了:
user, err := store.ReadUser(testutil.MustCreate(t, NewUsername, "joe123")) if got, want := user.Name.String(), "joe123"; got != want { t.Errorf("username=%v, want=%v", got, want)
神奇之处在于这个泛型 MustCreate 函数:
func MustCreate[RawInput, CreatedValue any]( t testing.TB, create func(RawInput) (CreatedValue, error), rawInput RawInput, ) CreatedValue { t.Helper() createdValue, err := create(rawInput) if err != nil { t.Fatalf("failed to create test value: %v", err) return createdValue
我在推理泛型时经常犯迷糊,所以下面逐步讲解这个函数的实现。在测试中,我这样调用 MustCreate:
testutil.MustCreate(t, NewUsername, "joe123")
上面的调用等价于下面这个更显式的版本:
testutil.MustCreate[string, Username](t, NewUsername, "joe123")
在这里调用 MustCreate 时,RawInput 是 string(原始用户名的类型),CreatedValue 是 Username(返回类型)。它等价于下面这样的实现:
func MustCreate( t testing.TB, create func(string) (Username, error), rawInput string, ) CreatedValue { t.Helper() createdValue, err := create(rawInput) if err != nil { t.Fatalf("failed to create test value: %v", err) return createdValue
扩展到多参数构造函数 🔗︎
那参数不止一个的构造函数呢?
func NewRange(start int, end int) (Range, error) { ... }
遇到这种情况,我会另外写一个名为 MustCreate2 的辅助函数:
range := testutil.MustCreate2(t, models.NewRange, 0, 5)
MustCreate2 的实现和 MustCreate 几乎一样,只是为第二个参数多加了一个类型:
func MustCreate2[RawInput1, RawInput2, CreatedValue any]( t testing.TB, create func(RawInput1, RawInput2) (CreatedValue, error), rawInput1 RawInput1, rawInput2 RawInput2, ) CreatedValue { t.Helper() createdValue, err := create(rawInput1, rawInput2) if err != nil { t.Fatalf("failed to create test value: %v", err) return createdValue
结论 🔗︎
MustCreate 模式算不上什么革命性创新,但这个技巧改善了我的 Go 测试,也减少了我开发工作中一些无谓的重复劳动。
我写了一本书,介绍帮助开发者提升写作水平的简单技巧。
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。