Golang Go语言基础 - 编写单元测试(译文)

发布于 1周前 作者 nodeper 来自 Go语言

Golang Go语言基础 - 编写单元测试(译文)

查看原文

在上一篇文章 "Grab JSON from an API" 中,我们探索了如何使用 HTTP 客户端以及如何解析 JSON 数据。本篇文章是 Go 语言主题的续篇,讲述如何编写单元测试。

1. Go 语言中的测试

Go 语言有一个自带的测试命令 go test ,还有一个标准 testing 测试包,它能够为你提供一个小却完整的测试体验。

这套标准工具链还包括了基准测试以及基于语句的代码覆盖率测试,类似与 NCover(.Net) 或者 Istanbul(Node.js)。

1.2 编写测试代码

和 Go 语言其它方面如格式化、命名规则一样,Go 语言的单元测试也显得个性十足。它的语法刻意规避了使用断言模式,并将值验证和行为检测的工作留给了开发人员。

这儿有一个例子,我们要对 main 包里的一个方法进行测试。我们已定义了一个名为 Sum 的出口函数,它接收两个整数参数,并将它们相加。

package main

func Sum(x int, y int) int { return x + y }

func main() { Sum(5, 5) }

我们在另一个单独的文件中编写测试代码。这个测试文件可以在其它的包(目录)中,或者在相同的包中(main)。以下是一个检测相加结果的单元测试:

package main

import “testing”

func TestSum(t *testing.T) { total := Sum(5, 5) if total != 10 { t.Errorf(“Sum was incorrect, got: %d, want: %d.”, total, 10) } }

Go 语言的测试函数有以下特征:

  • 只有唯一的参数,必须是 t *testing.T 类型
  • 必须以单词 Test 开头,再组合上首字母大写的单词或词组(一般是被测试的方法名称,如 TestValidateClient
  • 调用 t.Error 或者 t.Fail 方法指明测试失败(这里我使用了 t.Errorf 来提供更多的细节)
  • t.Log 可以用来提供一些失败信息以外的调试信息
  • 测试代码文件名必须是 _test 结尾的形式 something_test.go ,例如:addtion_test.go

如果你在同一个目录下既有代码也有测试代码,那么你就无法使用 go run *.go 的方式执行你的程序了。我一般会使用 go build 编译出可执行程序,再执行它。

你可能更习惯于使用 Assert 关键字进行验证工作,不过 The Go Programming Language 的作者们对于 Go 的断言方式做了许多很好的辩解。

当使用断言时:

  • 测试代码往往会让人觉得他们正在使用另一种语言(比如 RSpec/Mocha )
  • 错误输出看起来令人费解 "assert: 0 == 1"
  • 可能会产生大量的调用栈信息
  • 第一个断言失败后,测试代码会终止执行 - 会掩盖其它的失败可能

有一些类似 RSpec 或者 Assert 的 Go 语言第三方测试库。比如 stretchr/testify

测试表

“测试表”的概念是一组测试输入和输出值的映射。这是一个针对 Sum 函数的例子:

package main

import “testing”

func TestSum(t *testing.T) { tables := []struct { x int y int n int }{ {1, 1, 2}, {1, 2, 3}, {2, 2, 4}, {5, 2, 7}, }

for _, table := range tables {
	total := Sum(table.x, table.y)
	if total != table.n {
		t.Errorf("Sum of (%d+%d) was incorrect, got: %d, want: %d.", table.x, table.y, total, table.n)
	}
}

}

如果你想要制造一些错误使得测试无法通过,那么将 Sum 函数的返回部分改为 x * y 即可。

$ go test -v
=== RUN   TestSum
--- FAIL: TestSum (0.00s)
	table_test.go:19: Sum of (1+1) was incorrect, got: 1, want: 2.
	table_test.go:19: Sum of (1+2) was incorrect, got: 2, want: 3.
	table_test.go:19: Sum of (5+2) was incorrect, got: 10, want: 7.
FAIL
exit status 1
FAIL	github.com/alexellis/t6	0.013s

启动测试

有两种方式可以用来启动一个包内的测试代码。这些方法对于单元测试和集成测试是相同的。

  1. 在和测试文件相同的目录中:

    go test
    

    这会执行包内所有匹配 _test.go 名称的测试代码

    或者

  2. 采用完整的包名

    go test github.com/alexellis/golangbasics1
    

现在你可以执行 Go 语言单元测试了,可以使用 go test -v 获得更详细的输出,你能看到每条测试的 PASS/FAIL 信息,以及所有 t.Log 打印出的额外日志信息。

单元测试和集成测试的区别在于,单元测试通常独立于外部依赖,不会与网络、磁盘等产生交互。单元测试一般只关注函数的功能。

1.3 go test 的更多用法

语句( statement )覆盖率

go test 工具自带内建的代码语句覆盖率测试功能。想要用之前的代码例子尝试一下,输入以下命令即可:

$ go test -cover
PASS
coverage: 50.0% of statements
ok  	github.com/alexellis/golangbasics1	0.009s

较高的语句覆盖率比低覆盖率或者零覆盖率要好,不过这样量化也可能会产生误导。我们想保证我们不只是在执行语句,而且我们还验证了代码的行为和输出,而且在不符合逻辑的地方报错。如果你删除了之前例子代码中的 “ if ” 语句,它仍然会保持 50% 的测试覆盖率,却丧失了验证 “ Sum ” 方法行为的用处。

生成 HTML 格式的覆盖率测试报告

如果你使用接下来的两条命令,你就可以直观地看到你的程序哪些部分被覆盖到了,而哪些语句没有被覆盖到:

go test -cover -coverprofile=c.out
go tool cover -html=c.out -o coverage.html 

然后用浏览器打开 coverage.html 文件。

Go 编译时不会引入你的测试代码

还有一点,将 addition_test.go 这样的测试文件留在你的包目录中虽然略有些不自然。不过 Go 语言的编译器和链接器保证不会将你的测试文件编入任何它生成的二进制文件中。

下面有个例子,可以找出 net/http 包中的生成代码和测试代码。

$ go list -f={{.GoFiles}} net/http
[client.go cookie.go doc.go filetransport.go fs.go h2_bundle.go header.go http.go jar.go method.go request.go response.go server.go sniff.go status.go transfer.go transport.go]

$ go list -f={{.TestGoFiles}} net/http [cookie_test.go export_test.go filetransport_test.go header_test.go http_test.go proxy_test.go range_test.go readrequest_test.go requestwrite_test.go response_test.go responsewrite_test.go transfer_test.go transport_internal_test.go]

想要了解更多的基础内容可以阅读 Golang testing docs

1.4 脱离依赖

定义单元测试概念的关键点就是,它能够脱离运行时的依赖项或合作者。

这在 Go 语言中是通过接口来实现的,不过如果你有 C# 或者 Java 的背景,它们的接口看起来和 Go 会有些许不同。Go 语言中接口是隐含的,而不是一种强制措施。意味着实际的类并不需要知道接口的存在。

这意味着我们可以定义非常多的小接口,如 io.ReadCloser 它只包含两个方法分别来自于 Reader 和 Closer 接口:

Read(p []byte) (n int, err error)

Reader 接口

Close() error

Closer 接口

如果你在设计一个会被第三方使用的包,那么定义适当的接口就会显得非常有意义,因为其他人需要时,可以利用这些接口让单元测试代码能够不依赖于你的代码包。

接口的具体实现在函数调用时可以被替换。如果我们想要测试这个方法,我们可以提供一个实现了 Reader 接口的伪造类。

package main

import ( “fmt” “io” )

type FakeReader struct { }

func (FakeReader) Read(p []byte) (n int, err error) { // return an integer and error or nil }

func ReadAllTheBytes(reader io.Reader) []byte { // read from the reader… }

func main() { fakeReader := FakeReader{} // You could create a method called SetFakeBytes which initialises canned data. fakeReader.SetFakeBytes([]byte(“when called, return this data”)) bytes := ReadAllTheBytes(fakeReader) fmt.Printf("%d bytes read.\n", len(bytes)) }

在实现你自己的抽象前,去 Golang 文档中搜索一下是否已有现成可用的东西,总会是个不错的主意。对于上面的例子我们也可以使用标准库中的 bytes 包:

func NewReader(b []byte) *Reader

Go 语言的 testing/iotest 包提供了一些 Reader 的实现类,有些执行起来比较慢,有些会在读数据的中途产生错误。这些实现对于适应性测试都非常好用。

查看原文


更多关于Golang Go语言基础 - 编写单元测试(译文)的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang Go语言基础 - 编写单元测试(译文)的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang(Go语言)中,编写单元测试是确保代码质量和稳定性的重要手段。Go语言内置的testing包为编写和运行测试提供了强大的支持。

首先,你需要在你的Go项目中创建一个测试文件。通常,测试文件的命名规则是被测试文件名的后缀加上_test.go。例如,如果你的源文件是main.go,那么测试文件就应该命名为main_test.go

在测试文件中,你需要导入testing包,并编写测试函数。测试函数的命名必须以Test开头,并接收一个指向testing.T类型的指针作为参数。例如:

package main

import (
    "testing"
)

func TestAddition(t *testing.T) {
    result := 1 + 1
    expected := 2
    if result != expected {
        t.Errorf("Expected %d, but got %d", expected, result)
    }
}

在这个例子中,我们编写了一个简单的测试函数TestAddition,用于测试加法操作。如果结果不符合预期,t.Errorf将记录一个错误。

运行测试非常简单,只需在命令行中执行go test命令即可。Go工具链会自动查找所有以_test.go结尾的文件,并运行其中的测试函数。

编写单元测试不仅可以提高代码质量,还能在代码重构或添加新功能时提供安全保障。因此,建议每个Go开发者都掌握编写单元测试的基本技巧。

回到顶部