返回资讯中心
外部精选
软件工程
#开发者实时流

Go 测试中 MustCreate 模式的实用技巧

来源标题:The MustCreate Pattern for Go Tests

作者分享了一种让 Go 测试代码更易读、更少重复的 MustCreate 模式。该模式通过编写 MustCreateUsername 等辅助函数包装可能失败的构造函数,省去测试中冗长的错误检查代码。

Hacker News 最新mtlynch2026/11/6 · 今天11 分钟阅读

以下正文由 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 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →