Skip to content

测试和性能

作为一名合格的开发者,不应该在程序开发完之后才开始写测试代码。使用 Go 语言的测试 框架,可以在开发的过程中就进行单元测试和基准测试。和 go build 命令类似,go test 命 令可以用来执行写好的测试代码,需要做的就是遵守一些规则来写测试。而且,可以将测试无缝 地集成到代码工程和持续集成系统里。

单元测试

单元测试是用来测试包或者程序的一部分代码或者一组代码的函数。测试的目的是确认目标 代码在给定的场景下,有没有按照期望工作。一个场景是正向路经测试,就是在正常执行的情况 下,保证代码不产生错误的测试。这种测试可以用来确认代码可以成功地向数据库中插入一条工 作记录。

另外一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错 误。这种场景下的测试可能是对数据库进行查询时没有找到任何结果,或者对数据库做了无效的 更新。在这两种情况下,测试都要验证确实产生了错误,且产生的是预期的错误。总之,不管如 何调用或者执行代码,所写的代码行为都是可预期的。

在 Go 语言里有几种方法写单元测试。基础测试(basic test)只使用一组参数和结果来测试 一段代码。表组测试(table test)也会测试一段代码,但是会使用多组参数和结果进行测试。也 可以使用一些方法来模仿(mock)测试代码需要使用到的外部资源,如数据库或者网络服务器。 这有助于让测试在没有所需的外部资源可用的时候,模拟这些资源的行为使测试正常进行。最后, 9

在构建自己的网络服务时,有几种方法可以在不运行服务的情况下,调用服务的功能进行测试。

基础单元测试

go
 // 这个示例程序展示如何写基础单元测试
 package listing01

 import ( 
 "net/http"
 "testing"
 ) 

 const checkMark = "\u2713"
 const ballotX = "\u2717"

 // TestDownload 确认 http 包的 Get 函数可以下载内容
 func TestDownload(t *testing.T) { 
 url := "http://www.goinggo.net/feeds/posts/default?alt=rss"
 statusCode := 200

 t.Log("Given the need to test downloading content.")
 { 
 t.Logf("\tWhen checking \"%s\" for status code \"%d\"",
 url, statusCode)
 { 
 resp, err := http.Get(url)
 if err != nil { 
 t.Fatal("\t\tShould be able to make the Get call.",
 ballotX, err)
 } 
 t.Log("\t\tShould be able to make the Get call.",
 checkMark)

 defer resp.Body.Close()

 if resp.StatusCode == statusCode { 
 t.Logf("\t\tShould receive a \"%d\" status. %v",
 statusCode, checkMark)
 } else { 
 t.Errorf("\t\tShould receive a \"%d\" status. %v %v",
 statusCode, ballotX, resp.StatusCode)
 } 
 } 
 } 
 }

Go 语言的测试工具只会认为以_test.go 结尾的文件是测试文件。如果没有遵从这个约定,在包里 运行 go test 的时候就可能会报告没有测试文件。一旦测试工具找到了测试文件,就会查找里 面的测试函数并执行。

可以看到测试函数的名字是 TestDownload。一个测试函数 必须是公开的函数,并且以 Test 单词开头。不但函数名字要以 Test 开头,而且函数的签名必 须接收一个指向 testing.T 类型的指针,并且不返回任何值。如果没有遵守这些约定,测试框 架就不会认为这个函数是一个测试函数,也不会让测试工具去执行它。

指向testing.T类型的指针很重要。这个指针提供的机制可以报告每个测试的输出和状态。 测试的输出格式没有标准要求。我更喜欢使用 Go 写文档的方式,输出容易读的测试结果。对我 来说,测试的输出是代码文档的一部分。测试的输出需使用完整易读的语句,来记录为什么需要 这个测试,具体测试了什么,以及测试的结果是什么。

表组测试

如果测试可以接受一组不同的输入并产生不同的输出的代码,那么应该使用表组测试的方法 进行测试。表组测试除了会有一组不同的输入值和期望结果之外,其余部分都很像基础单元测试。 测试会依次迭代不同的值,来运行要测试的代码。每次迭代的时候,都会检测返回的结果。这便 于在一个函数里测试不同的输入值和条件

go
 // 这个示例程序展示如何写一个基本的表组测试
 package listing08

 import ( 
 "net/http"
 "testing"
 ) 

 const checkMark = "\u2713"
 const ballotX = "\u2717"

 // TestDownload 确认 http 包的 Get 函数可以下载内容
 // 并正确处理不同的状态
 func TestDownload(t *testing.T) { 
 var urls = []struct { 
 url string
 statusCode int
 }{
 { 
 "http://www.goinggo.net/feeds/posts/default?alt=rss",
 http.StatusOK,
 },
 { 
 "http://rss.cnn.com/rss/cnn_topstbadurl.rss",
 http.StatusNotFound,
 },
 } 

 t.Log("Given the need to test downloading different content.")
 { 
 for _, u := range urls { 
 t.Logf("\tWhen checking \"%s\" for status code \"%d\"",
 u.url, u.statusCode)
 { 
 resp, err := http.Get(u.url)
 if err != nil { 
 t.Fatal("\t\tShould be able to Get the url.",
 ballotX, err)
 } 
 t.Log("\t\tShould be able to Get the url",
 checkMark)

 defer resp.Body.Close()

 if resp.StatusCode == u.statusCode { 
 t.Logf("\t\tShould have a \"%d\" status. %v",
 u.statusCode, checkMark)
 } else { 
 t.Errorf("\t\tShould have a \"%d\" status %v %v",
 u.statusCode, ballotX, resp.StatusCode)
 } 
 } 
 } 
 } 
 }

模仿调用

不能总是假设运行测试的机器可以访问互联网。此外,依赖不属于你的或者你无法操作的服 务来进行测试,也不是一个好习惯。这两点会严重影响测试持续集成和部署的自动化。如果突然 断网,导致测试失败,就没办法部署新构建的程序。

为了修正这个问题,标准库包含一个名为 httptest 的包,它让开发人员可以模仿基于 HTTP 的网络调用。模仿(mocking)是一个很常用的技术手段,用来在运行测试时模拟访问不 可用的资源。包 httptest 可以让你能够模仿互联网资源的请求和响应。

go
 // 这个示例程序展示如何内部模仿 HTTP GET 调用
 // 与本书之前的例子有些差别
 package listing12

 import ( 
 "encoding/xml"
 "fmt"
 "net/http"
 "net/http/httptest"
 "testing"
 ) 

 const checkMark = "\u2713"
 const ballotX = "\u2717"

 // feed 模仿了我们期望接收的 XML 文档
 var feed = `<?xml version="1.0" encoding="UTF-8"?>
 <rss>
 <channel>
 <title>Going Go Programming</title>
 <description>Golang : https://github.com/goinggo</description>
 <link>http://www.goinggo.net/</link>
 <item>
 <pubDate>Sun, 15 Mar 2015 15:04:00 +0000</pubDate>
 <title>Object Oriented Programming Mechanics</title>
 <description>Go is an object oriented language.</description>
 <link>http://www.goinggo.net/2015/03/object-oriented</link>
 </item>
 </channel>
 </rss>`

 // mockServer 返回用来处理请求的服务器的指针
 func mockServer() *httptest.Server { 
 f := func(w http.ResponseWriter, r *http.Request) { 
 w.WriteHeader(200)
 w.Header().Set("Content-Type", "application/xml")
 fmt.Fprintln(w, feed)
 } 

 return httptest.NewServer(http.HandlerFunc(f))
 }

测试服务端点

服务端点(endpoint)是指与服务宿主信息无关,用来分辨某个服务的地址,一般是不包含 宿主的一个路径。如果在构造网络 API,你会希望直接测试自己的服务的所有服务端点,而不用 启动整个网络服务。包 httptest 正好提供了做到这一点的机制。

go
 // 这个示例程序实现了简单的网络服务
 package main

 import ( 
 "log"
 "net/http"

 "github.com/goinaction/code/chapter9/listing17/handlers"
 ) 

 // main 是应用程序的入口
 func main() { 
 handlers.Routes()

 log.Println("listener : Started : Listening on :4000")
 http.ListenAndServe(":4000", nil)
 }

代码调用了内部 handlers 包的 Routes 函数。这个函数为托管的网络服务设置了一个服务端点。

go
 // handlers 包提供了用于网络服务的服务端点
 package handlers

 import ( 
 "encoding/json"
 "net/http"
 ) 

 // Routes 为网络服务设置路由
 func Routes() { 
 http.HandleFunc("/sendjson", SendJSON)
 } 

 // SendJSON 返回一个简单的 JSON 文档
 func SendJSON(rw http.ResponseWriter, r *http.Request) { 
 u := struct { 
 Name string
 Email string
 }{
 Name: "Bill",
 Email: "bill@ardanstudios.com",
 } 

 rw.Header().Set("Content-Type", "application/json")
 rw.WriteHeader(200)
 json.NewEncoder(rw).Encode(&u)
 }

现在有了包含一个服务端点的可用的网络服务,我们可以写单元测试来测试这个服务端点

go
 // 这个示例程序展示如何测试内部服务端点
 // 的执行效果
 package handlers_test

 import ( 
 "encoding/json"
 "net/http"
 "net/http/httptest"
 "testing"

 "github.com/goinaction/code/chapter9/listing17/handlers"
 ) 

 const checkMark = "\u2713"
 const ballotX = "\u2717" 

 func init() { 
 handlers.Routes()
 } 

 // TestSendJSON 测试/sendjson 内部服务端点
 func TestSendJSON(t *testing.T) { 
 t.Log("Given the need to test the SendJSON endpoint.")
 { 
 req, err := http.NewRequest("GET", "/sendjson", nil)
 if err != nil { 
 t.Fatal("\tShould be able to create a request.",
 ballotX, err)
 } 
 t.Log("\tShould be able to create a request.",
 checkMark)

 rw := httptest.NewRecorder()
 http.DefaultServeMux.ServeHTTP(rw, req)

 if rw.Code != 200 { 
 t.Fatal("\tShould receive \"200\"", ballotX, rw.Code)
 } 
 t.Log("\tShould receive \"200\"", checkMark)

 u := struct { 
 Name string
 Email string
 }{}

 if err := json.NewDecoder(rw.Body).Decode(&u); err != nil { 
 t.Fatal("\tShould decode the response.", ballotX)
 } 
 t.Log("\tShould decode the response.", checkMark)

 if u.Name == "Bill" { 
 t.Log("\tShould have a Name.", checkMark)
 } else { 
 t.Error("\tShould have a Name.", ballotX, u.Name)
 } 

 if u.Email == "bill@ardanstudios.com" { 
 t.Log("\tShould have an Email.", checkMark)
 } else { 
 t.Error("\tShould have an Email.", ballotX, u.Email)
 } 
 } 
 }

这次包的名字也使用_test 结尾。如果包使用这种方式命名,测试代码只能访问包里公开的标识符。即便测试代码文件和被测试的代码放在同一个文件夹中,也只能访问公开的标识符。

示例

Go 语言很重视给代码编写合适的文档。专门内置了 godoc 工具来从代码直接生成文档。这个工具的另一个特性是示例代码。示例代码给文档和测试都增加了一个可以扩展的维度。开发人员可以创建自己的示例,并且在包的 Go 文档里展示。

go
 // 这个示例程序展示如何编写基础示例
 package handlers_test

 import ( 
 "encoding/json"
 "fmt"
 "log"
 "net/http"
 "net/http/httptest"
 ) 

 // ExampleSendJSON 提供了基础示例
 func ExampleSendJSON() { 
 r, _ := http.NewRequest("GET", "/sendjson", nil)
 rw := httptest.NewRecorder()
 http.DefaultServeMux.ServeHTTP(rw, r)

 var u struct { 
 Name string
 Email string
 } 

 if err := json.NewDecoder(w.Body).Decode(&u); err != nil { 
 log.Println("ERROR:", err)
 } 

 // 使用 fmt 将结果写到 stdout 来检测输出
 fmt.Println(u)
 // Output:
 // {Bill bill@ardanstudios.com}
}

对于示例代码,需要遵守一个规则。示例代码的函数名字必须基于已经存在的公开的函数或 者方法。我们的示例的名字基于 handlers 包里公开的 SendJSON 函数。如果没有使用已经存 在的函数或者方法,这个示例就不会显示在包的 Go 文档里。 写示例代码的目的是展示某个函数或者方法的特定使用方法。为了判断测试是成功还是失 败,需要将程序最终的输出和示例函数底部列出的输出做比较

基准测试

基准测试是一种测试代码性能的方法。想要测试解决同一问题的不同方案的性能,以及查看 哪种解决方案的性能更好时,基准测试就会很有用。基准测试也可以用来识别某段代码的 CPU 或者内存效率问题,而这段代码的效率可能会严重影响整个应用程序的性能。许多开发人员会用 基准测试来测试不同的并发模式,或者用基准测试来辅助配置工作池的数量,以保证能最大化系 统的吞吐量。和单元测试文件一样,基准测试的文件名也必须以_test.go 结尾。同时也必须导入 testing 包。

go
 // BenchmarkSprintf 对 fmt.Sprintf 函数
 // 进行基准测试
 func BenchmarkSprintf(b *testing.B) { 
 number := 10

 b.ResetTimer()

 for i := 0; i < b.N; i++ { 
 fmt.Sprintf("%d", number)
 } 
 }

可以看到第一个基准测试函数,名为 BenchmarkSprintf。 基准测试函数必须以 Benchmark 开头,接受一个指向 testing.B 类型的指针作为唯一参数。 为了让基准测试框架能准确测试性能,它必须在一段时间内反复运行这段代码,所以这里使用了 for 循环

基准测试框架默认会在持续 1 秒的时间内,反复调用需要测试的函数。测试框架每次调用测 试函数时,都会增加 b.N 的值。第一次调用时,b.N 的值为 1。需要注意,一定要将所有要进 行基准测试的代码都放到循环里,并且循环要使用 b.N 的值。否则,测试的结果是不可靠的。

在运行单元测试和基准测试时,存在很多参数,可以参阅官方文档进行查看

Released under the MIT License.