单元测试时用来测试包或者程序的一部分代码或者一组代码的函数。
在Go语言中,其单元测试主要包括:基础测试,其只使用一组参数和结果来测试一段代码。表组测试也会测试一段代码,但是会使用多组参数和结果进行测试。也可以使用一些方法来模拟测试代码需要用到的外部资源,如数据库或者网络服务器。这有助于让测试在没有所需的外部资源可用的时候,模拟这些资源的行为让测试正常进行。
1、基础单元测试
package chapter9
import (
"net/http"
"testing"
)
const checkMark = "\u2713"
const ballotX = "\u2717"
func TestDownload(t *testing.T) {
url := "http://www.goinggo.net/index.html"
statusCode := 200
t.Log("Given the need to test downloading content.")
{
t.Logf("\t When checking \"%s\" for status code \"%d\"", url, statusCode)
{
resp, err := http.Get(url)
if err != nil {
t.Fatal("\t\t Should be able to make the Get call.", ballotX, err)
}
t.Fatal("\t\t Should be able to make the Get call.", checkMark)
defer resp.Body.Close()
if resp.StatusCode == statusCode {
t.Logf("\t\t Should receive a \"%d\" status. %v", statusCode, checkMark)
} else {
t.Errorf("\t\t Should reveive a \"%d\" status. %v %v",statusCode, ballotX, resp.StatusCode)
}
}
}
}
通过运行命令 go test -v 来运行这个测试(-v 提供冗余输出)
测试文件要以 _test.go 命名结束,这主要是go语言的测试工具只会认以 _test.go 结尾的文件是测试文件。
2、表组测试
如果测试可以接受一组不同的输入并产生不同的输出代码,那么应该使用表组测试的方法进行测试。表组测试除了会有一组不同的输入值和期望结果之外,其余部分都很像基础单元测试。
package chapter9
import (
"net/http"
"testing"
)
const checkMark = "\u2713"
const ballotX = "\u2717"
func TestDownload(t *testing.T) {
var urls = []struct{
url string
statusCode int
}{
{
"https://www.baidu.com/",
http.StatusOK,
},
{
"http://res.cnn.com/rss/cnn_topstabdurl.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\t Should be able to Get the url.", ballotX, err)
}
t.Log("\t\t Should be able to Get the url", checkMark)
defer resp.Body.Close()
if resp.StatusCode == u.statusCode {
t.Logf("\t\t Should have a \"%d\" status. %v",u.statusCode, checkMark)
} else {
t.Errorf("\t\t Should have a \"%d\" status %v %v", u.statusCode, ballotX, resp.StatusCode)
}
}
}
}
}
3、模仿调用
有些程序的测试可能需要连接互联网,才能保证测试运行成功。不能总是假设运行测试的机器可以访问互联网,如果突然断网,则必定影响到测试的进度。
为了解决这个问题,Go 的标准库中包含一个名为 httptest 的包,它让开发人员可以模仿基于HTTP 网络调用。模仿( mocking) 是一个很常用的技术手段,用来在测试时模拟访问不可用的资源。
package chapter9
import (
"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))
}
func TestDownload(t *testing.T) {
statusCode := http.StatusOK
server := mockServer()
defer server.Close()
t.Log("Given the need to test downloading content.")
{
t.Logf("\t When checking \"%s\" for status code \"%d\"", server.URL, statusCode)
{
resp, err := http.Get(server.URL)
if err != nil{
t.Fatal("\t\t Should be able to make the Get call.", ballotX, err)
}
t.Log("\t\t Should be able to make the Get call.", checkMark)
defer resp.Body.Close()
if resp.StatusCode != statusCode {
t.Fatalf("\t\t Should receive a \"%d\" status. %v %v", statusCode, ballotX, resp.StatusCode)
}
t.Logf("\t\t Should receive a \"%d\" status. %v", statusCode, checkMark)
}
}
}
4、测试服务端点
服务端点是指与服务宿主信息无关,用来分辨某个服务的地址,一般是不包含宿主的一个路径。如果在构造网络API,你会希望直接测试自己的服务的所有服务端点,而不用启动整个网络服务。包 httptest 正好提供了做到这一点的机制。
// Package handlers provides the endpoints for the web service.
package handlers
import (
"encoding/json"
"net/http"
)
// Routes sets the routes for the web service.
func Routes() {
http.HandleFunc("/sendjson", SendJSON)
}
// SendJSON returns a simple JSON document.
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)
}
package chapter9
import (
"encoding/json"
"myExtends/handlers"
"net/http"
"net/http/httptest"
"testing"
)
const checkMark = "\u2713"
const ballotX = "\u2717"
func init() {
handlers.Routes()
}
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("\t Should be able to create a request.", ballotX, err)
}
t.Log("\t Should be able to create a request.",checkMark)
rw := httptest.NewRecorder()
http.DefaultServeMux.ServeHTTP(rw, req)
if rw.Code != 200 {
t.Fatal("\t Should 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("\t Should decode the response.", ballotX)
}
t.Log("\t Should decode the respons.", checkMark)
if u.Name == "Bill"{
t.Log("\t Should have a name.", checkMark)
} else {
t.Error("\t Should have a name.", ballotX, u.Name)
}
if u.Email == "bill@ardanstudios.com" {
t.Log("\t Should have an Email.", checkMark)
} else {
t.Error("\t Should have an Email.", ballotX, u.Email)
}
}
}
从上面代码可知, http.NewRequest() 函数创建了一个 http.Request 值。这个 Request 值使用 GET 方法调用 /sendjson 服务端点的相应。由于这个调用使用的是 GET 方法,第三个发送数据的参数被传为 nil。
httptest.NewRecoder 函数创建了一个 http.ResponseRecorder 值。有了http.Request 和 http.ResponseRecorder 这两个值,就可以调用服务默认的多路选择器(mux)的 ServeHttp 方法。调用这个方法模仿了外部客户端对 /sendjson 服务端点的请求。