Go Basic:理解Go中的测试和基准测试

保证代码质量和性能。

full lessons here👇:
https://programmerscareer.com/zh-cn/golang-basic-skill/

由AI生成,可能有错误,仅供参考

Topic 1.1: Go 中的测试介绍

测试代码是确保其质量和正确性的基本步骤。测试验证您的代码是否按照预期行为,并使其更不容易出错。如果您在团队中工作,它还可以确保其他人修改不会破坏现有功能。

Go 具有一个内置包叫做 **testing**,它提供了支持自动化测试的功能。Go 的哲学鼓励良好的测试代码,而 testing 包反映了这个哲学,提供了易于使用的特性让您将测试集成到编码实践中。

在 Go 中,每个测试都与一个测试函数相关,这些规则:

  • 它必须在以 _test.go 结尾的文件中。
  • 测试函数必须以 Test 开头。
  • 测试函数只能接受一个参数,即 t *testing.T

下面是一个简单的测试示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package stringutil

import "testing"

func TestReverse(t *testing.T) {
cases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{"Hello, 世界", "界世 ,olleH"},
{"", ""},
}
for _, c := range cases {
got := Reverse(c.in)
if got != c.want {
t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
}
}
}

Topic 1.2: 单元测试

为了确保代码的可靠性,您应该对其最小可测试组件(称为“单元”)进行测试。这类测试被称为“单元测试”。

每个单元测试都是独立的测试用例,验证特定代码部分的功能。在 Go 中,单元测试通常使用 testing 包,正如之前所解释的那样。

当我们创建单元测试时,我们应该遵循以下指南:

  • 每个单元测试都应该是独立的,可以单独运行。
  • 测试应该简单、清晰和易于理解。
  • 测试应该清楚地表明它正在测试什么,以及预期结果。

下面是一个基本示例,使用 strconv.Itoa 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
package gorilla

import (
"strconv"
"testing"
)

func TestItoa(t *testing.T) {
str := strconv.Itoa(454545)
if str != "454545" {
t.Errorf("Expected strconv.Itoa to return '454545' but got %v", str)
}
}

Topic 1.3: 表驱动测试

测试不同的场景时,经常需要编写多个测试,但它们之间的逻辑几乎相同。这可能会导致大量重复代码。表驱动测试允许您使用单个测试来测试不同输入值。

表驱动测试使用一个单个测试函数,它遍历一个测试用例表。该表是一个数组,包含结构体值,每个结构体表示一个测试用例。

让我们看看一个表驱动测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package stringutil

import "testing"

func TestReverse(t *testing.T) {
var tests = []struct {
input string
expected string
}{
{"hello", "olleh"},
{"world", "dlrow"},
{"", ""},
}
for _, tt := range tests {
actual := Reverse(tt.input)
if actual != tt.expected {
t.Errorf("Reverse(%q) = %q; expected %q", tt.input, actual, tt.expected)
}
}
}

在这个示例中,我们创建了多个测试用例来测试函数 Reverse。输入和预期值都定义在 tests 变量中。这些建议在 for 循环中运行,使用 Reverse 函数将输入值调用,并检查输出结果。

如果函数在任何测试用例中失败,它将打印一个错误消息,包括它失败的输入、实际输出和预期输出。

这使得检测哪个测试用例失败了,以及为什么变得更容易。

1.4 Mocking 和接口

在测试复杂应用程序时,我们经常需要处理外部服务、数据库和 API,这些服务通常独立于我们的代码库。在测试中,这些服务可能会有不可预测的行为甚至 unavailable。这个时候,mocking就来了。

mocking 是将软件的一部分(通常是那些具有外部依赖关系的部分)替换为 dummy 实现,以便进行测试。

Go 提供了一种优雅地处理这种情况的方式,即使用 接口。Go 接口使得可以在测试中将实际依赖关系替换为 mock 对象。

下面是一个简单的示例。让我们考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Downloader interface {
Download(string) ([]byte, error)
}

type FileDownloader struct{}
func (fd FileDownloader) Download(url string) ([]byte, error) {
// 文件下载逻辑
}

func processFile(d Downloader) {
content, err := d.Download("http://example.com")
if err != nil {
fmt.Println(err)
return
}
// 处理内容…
}

在这个示例中,FileDownloader 是我们实际实现的下载文件的方式。函数 processFile 接受一个类型为 Downloader 的参数,这个接口是 FileDownloader 实现的。

现在,如果我们想测试 processFile 函数而不需要访问外部网站,我们就需要创建一个 mock 实现的 Downloader 接口。我们将其称为 MockDownloader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type MockDownloader struct {
mockContent []byte
mockError error
}
func (md MockDownloader) Download(_ string) ([]byte, error) {
return md.mockContent, md.mockError
}

func Test_processFile(t *testing.T) {
d := &MockDownloader{
mockContent: []byte("test content"),
mockError: nil,
}
processFile(d)
// 检查 processFile 是否正确地处理了内容…
}

Test_processFile 测试函数中,我们创建一个 MockDownloader 实例并将其传递给 processFile 函数。这使得函数可以独立于外部依赖关系进行测试。

1.5 测试覆盖率

Go 中的测试覆盖率是衡量代码被测试的程度的指标。它是一个有用的指标,可以帮助您了解您的测试范围。

要计算 Go 的测试覆盖率,我们使用内置的 go test 工具和 -cover 标志。

1
go test -cover

这个命令,在您的包目录下运行时,将执行测试并返回以下语句:

1
2
PASS
coverage: 80% of statements

这个百分比告诉您代码被“覆盖”的程度。

如果您想了解更多关于测试覆盖率的信息,可以使用 -coverprofile 标志来生成一个详细的报告。

1.6 Mocking 和 Benchmark

在上一节中,我们学习了如何使用 mocking 来测试我们的函数。在这个部分,我们将学习如何使用 benchmark 来评估我们的函数性能。

benchmark 是一种评估函数性能的方式。它可以帮助您了解函数执行时间、内存占用等信息。

要使用 benchmark,需要编写一个名为 Benchmark 的函数,该函数接收一个 testing.B 对象作为参数。

例如,我们可以编写一个 benchmark 函数来评估 Reverse 函数的性能:

1
2
3
4
5
func BenchmarkReverse(b *testing.B) {
for i := 0; i < b.N; i++ {
Reverse("The quick brown fox jumps over the lazy dog")
}
}

要运行 benchmark,可以使用 go test 命令和 -bench 标志:

1
go test -bench=.

这个命令将执行所有在当前目录下的 benchmark。

通过分析 benchmark 的结果,您可以对函数进行调整以提高其性能。

Topic 1.7: Benchmarking Different Parts of the Code

有时候,你的整个函数可能不是瓶颈,而是某个小部分导致性能下降。在这种情况下,benchmarking整个函数不太有帮助。反之,benchmarking特定的代码部分就很有用了。

然而,在 Go 中,benchmarking特定的代码部分有一点儿 tricky,因为 b.N 循环控制函数执行的次数。如果你想 benchmark某个函数的特定部分,你需要确保那个部分被执行 b.N 次。

下面是一个简单的方法:

1
2
3
4
5
6
7
8
9
10
11
12
package stringutils

import "testing"

func BenchmarkReverseSubstring(b *testing.B) {
// 数据设置
data := "The quick brown fox jumps over the lazy dog"
b.ResetTimer() // 重置计时器,排除数据设置时间
for i := 0; i < b.N; i++ {
Reverse(data[i%len(data)/2 : len(data)/2 + i%len(data)/2])
}
}

在这个修改后的 benchmark 中,只有字符串的前半部分被反转,这模拟了 benchmarking某个函数的特定部分的过程。

当你运行这个 benchmark 时,可以将其输出与之前的benchmark输出进行比较,从而了解函数中的潜在瓶颈可能在哪里。

请注意,benchmarking本身会增加一些开销,所以在实际程序中最好不要太多地使用 benchmark。它们是你的开发和测试环境中的工具,帮助你编写高效的代码。

Topic 1.8: Analysis and Parsing Benchmark Results

一旦我们已经 benchmarked我们的 Go 代码,我们就需要了解结果。解析benchmark结果涉及理解每个输出数字的含义,以及如何根据代码变化而改变这些数字。

当你在 Go 中运行一个 benchmark 时,通常会获得以下类型的输出:

1
BenchmarkReverse-4 1000000 1238 ns/op

下面是这个输出的解释:

  • BenchmarkReverse-4:这是测试函数的名称。 -4 表示该benchmark使用了 4 个 CPU。
  • 1000000:这是操作被执行的次数。
  • 1238 ns/op:这是操作的成本。在这里,它说我们的 Reverse 函数平均需要 1238 nanoseconds 执行一次操作。

armed with this knowledge,你现在可以比较 benchmark 输出前后,以检查代码变化是否提高了性能或使其更糟。您还可以比较不同函数或函数部分的性能,以找到瓶颈。

Topic 1.9: Overview and Importance of Good Practices

作为软件工程师,我们总是应该努力编写干净、有效和可管理的代码。我们所学到的所有技术,如测试、benchmarking 和分析结果,这些都是更广泛的议程,强调编码良好的实践。

这些实践为什么重要?以下是一些原因:

  • 可读性和可维护性:经过测试和 benchmark 的代码可能是干净和可管理的。这使得它更容易被其他人(或你的未来自)阅读和维护。
  • 性能:benchmarking 帮助你找到程序中的瓶颈。通过不断优化这些区域,你确保你的代码能够高效地执行。
  • 可靠性:通过测试你的代码并修复错误,你增加了程序的可靠性。
  • 协作:良好的编码实践,如写出描述性的注释、使用有意义的变量名和逻辑组织代码,这些都使得与其他开发者合作变得更容易。

Topic 1.10: Assessment

现在是时候将你的知识付诸行动了。让我们快速进行一个评估:

  1. 在自己的语言中,写出如何 benchmark Go 函数的简短描述。
  2. 什么信息一个典型的 Go 测试输出提供?请给出一个示例,并解释每个部分的含义。
  3. 为什么遵循良好的编码实践如此重要?有什么这些实践?

English post: https://programmerscareer.com/go-basic-13/
作者:Wesley Wei – Twitter Wesley Wei – Medium
注意:本文为作者原创,转载请注明出处。

Go基础:理解 Time, Epoch, Time Formatting Parsing, Random Numbers, Number Parsing, URL Parsing, sha256 Hashes, base64 Encoding in Go Go基础:理解Go中的命令行、环境变量

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×