Go测试深入探索:你需要知道的一切都在这里

超级欧派课程 2024-04-14 09:02:51

引言

本文旨在探索关键的 Go 测试元素,如并行测试、子测试、拆卸函数和测试辅助函数,以提高你的测试技能。测试在软件开发过程中扮演着至关重要的角色,在 Go 中进行测试非常简单。通常,你可以按照以下步骤进行:

创建以 _test.go 结尾的文件。创建一个符合 TestXxx(t *testing.T)​ 签名的函数。使用 go test​ 命令执行测试(或在你的 IDE 中点击按钮)。

然而,你是否考虑过可以做得更多呢?在本文中,我们将深入探讨在 Go 中编写测试的附加技术,超越基本步骤。这些方法旨在使你的测试不仅功能齐全,而且易于维护和高效。

1. 处理失败标记

首先,我们来讨论如何在 Go 中标记测试失败。你可以选择使用以下方法之一:

​t.Fail()​: 该方法仅将测试标记为失败,但不会停止测试的继续运行。​t.FailNow()​: 该方法将测试标记为失败,并立即使用 runtime.Goexit()​ 停止任何进一步的执行。​t.Errorf()​: 这是一个结合了 t.Logf()​ 记录错误消息和 t.Fail()​ 标记测试失败的两合一方法。​t.Fatalf()​: 这是另一个结合了 t.Logf()​ 记录错误消息和 t.FailNow()​ 立即停止测试的组合方法。

如果你在一个函数中有多个独立的测试用例,并且希望即使其中一个用例失败也继续运行其他用例,那么 t.Errorf()​ 是合适的选择。例如:

然而,如果你的测试用例彼此依赖,并且如果一个用例失败就需要停止所有测试,那么你可能需要考虑使用 t.Fatalf()​。例如:

2. 表驱动测试

让我们来看一个基本的 Add 函数的单元测试,它接受一个整数列表作为输入。

在我们的 TestAdd​ 函数中,目前我们正在测试两个用例:

现在,想象一下,如果你想要测试不止这两个场景,比如10个场景呢?继续使用这种方式会导致冗余且难以维护的代码。

更高效的替代方法是表驱动测试,它既更易于阅读,又更易于更新:

通过使用表驱动方法,添加新的测试场景变得非常简单。只需更新 testCases​ 切片,特别是匿名结构体,就可以避免重复的代码。这样可以保持测试代码整洁,并且更容易查看所有的测试场景。

然而,目前的方法存在一个小问题,即在使用 go test -v​ 命令进行详细输出时,输出如下所示:

这并没有提供太多信息,只是说明测试通过了,并没有指示测试用例的数量,也没有指出具体哪个测试用例可能失败。

举个例子,假设有一个测试用例没有通过:

当测试用例失败时,输出并不那么有帮助,我们无法确定哪个用例出错了,也无法知道其输入是什么。

这就是子测试的价值所在,使用子测试可以提高测试报告的质量,在出现问题时提供更多信息和实用性。

3. 子测试:运行多个测试用例

为了充分利用Go的测试包中的子测试功能,我们首先介绍一个新函数:t.Run(name string, f func(t *testing.T)) (isSuccess bool)​。

函数t.Run()​使用提供的名称生成一个子测试,并将函数f​作为一个独立的goroutine运行。尽管每个子测试在它们自己的goroutine中执行,但它们是按顺序执行的(稍后我们将探讨并行执行)。

下面是如何修改现有的测试以包含子测试:

现在,我们为每个测试用例创建了一个子测试,并在其中运行相应的测试逻辑。这样,每个测试用例都将以其指定的名称进行报告,并且我们将获得更详细的输出。

让我们运行测试看看结果:

现在,我们可以看到每个测试用例的名称,并且测试结果更加详细和易于理解。

4. 在 Go 1.22 中修复 For 循环

为了并行运行子测试,可以使用t.Parallel()​来启用并行模式。

这对于拥有大量独立测试用例的情况特别有用。通过并行运行它们,可以加快整个测试过程的速度。

看看如何实现:

“好的,等等。为什么循环中有一个tc := tc​?”

好吧,这是为了处理与闭包相关的常见问题。

这一行额外的代码确保每次循环迭代都有自己独立的tc​副本,避免与原始的循环变量发生冲突。

困惑吗?下面是一个例子来澄清一切:

现在,你期望它输出什么?你可能会认为是“0、1、2”,对吗?

错了。

实际上,你会看到“3 3 3”,因为所有的goroutine都引用了循环结束后的同一个i的最终值,即3。为了解决这个问题,在循环开始后立即添加​i := i`。

好消息是,LoopVar问题将在Go 1.22中得到解决。你可以在这篇文章中阅读更多相关内容。

5. 其他单元测试概念

我们已经介绍了基础知识,但是在 Go 中进行单元测试还提供了更多功能。如果你有高级需求,你会发现还有其他类似于不同单元测试框架中提供的工具。

Helper

首先,让我们讨论一下 t.Helper()​ 函数。根据官方描述:“Helper 标记调用函数为测试辅助函数。在打印文件和行信息时,该函数将被跳过。”

起初,我并不明白这个函数的作用。为什么我要隐藏函数细节?但是当我开始创建自己的测试工具(如断言)时,t.Helper()​ 的作用变得清晰起来。

举个例子,假设你正在使用一个名为 AssertNil​ 的辅助函数来确保另一个函数返回一个 'nil' 错误。代码如下所示:

当你像之前一样运行 go test -v​ 命令时,你会看到以下输出:

“那有什么大不了的?看起来报告工作得很好。”

好吧,这里有个问题。

报告过于精确,但并没有那么有用。你会注意到,错误发生在 write_test.go:10​,这是 AssertNil​ 函数的位置。但是你真正想要的是错误发生在 TestF​ 函数中。

这就是 t.Helper()​ 的用途。当你在 AssertNil​ 函数中添加 t.Helper()​ 后,输出将更有意义:

现在,错误发生在 write_test.go:19​,这是 TestF​ 函数中调用 AssertNil​ 的位置。

这个小技巧对于更复杂的测试工具函数非常有用,因为它们可能包含多个辅助函数调用和内部逻辑。

Cleanup (teardown)

Go 1.14 中的 t.Cleanup()​ 函数一开始可能会让你困惑,它允许你定义一个在测试结束后运行的清理函数。

“延迟执行(defer)不也能做同样的事情吗?有什么大不了的?”

让我们仔细看一下。

现在,相较于使用延迟执行(defer)来清理函数,t.Cleanup()​ 的优势在于它可以更好地控制清理的时机。清理函数将在测试函数返回之前执行,而不仅仅是在函数退出时执行。

“有何用途呢?

为了明确这一点,让我们看一个涉及名为ConnectDB(t *testing.T)​的函数的示例。该函数建立一个数据库连接,并返回该连接:

现在,让我们运行这个测试,观察t.Cleanup()​在实际情况下的行为:

我们将 t *testing.T​ 传递给 ConnectDB​ 函数,并设置了一个清理函数,而不是使用 defer conn.Close()​。

只有在整个测试完成后,才会显示 "关闭数据库"​ 的信息,而不是在 ConnectDB()​ 完成部分后。

这确保了我们在正确的时间关闭数据库连接,而不是过早关闭。如果使用 defer​,则会在 ConnectDB()​ 退出时关闭连接,这可能不是你想要的结果。

更多

如果你想了解更多关于 Golang 的知识,请关注我的《Golang实用技巧》专栏,它将为你揭秘生产环境最佳实践,带你探索高并发编程的实用教程。从分享实用的Golang小技巧到深入剖析实际应用场景,让你成为真正的Golang大师。无论你是初学者还是经验丰富的开发者,这里都有你所需要的灵感和知识。让我们一同探索Golang的无限可能!

0 阅读:0

超级欧派课程

简介:感谢大家的关注