package_test
一般Go的package名称与所在目录名称一致。
Go规定在同一个目录下的文件都必须属于同一个package,但是除了_test.go文件外
。
所以,将你的测试文件放到package_test包内,如标准库里的 fmt_test.go,它的包名: package fmt_test
然后引用要测试的包,这样就会让我们更关注测试对外可见的方法。因为,如果在同一个包里,是可以访问私有方法,而我们不需要测试这些私有方法,则可以通过包名不同,来屏蔽访问。如fmt_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| package fmt_test
import (
. "fmt"
)
func TestFmtInterface(t *testing.T) {
var i1 interface{}
i1 = "abc"
s := Sprintf("%s", i1)
if s != "abc" {
t.Errorf(`Sprintf("%%s", empty("abc")) = %q want %q`, s, "abc")
}
}
|
_internal_test.go
上面的package_test是用来测试对外可见的方法。如果,我们要测试内部方法,就创建*_internal_test.go这样的文件。使用同一个package。这种对于tdd,确保每个方法的测试很有用。
可以参考标准库 time包里的internal_test.go
对了,go build的时候 不会编译以_test.go结尾的文件,只有go test的时候才会编译。
table driven testing
这种方式其实就把,要测试的input和result放到,一个[]struct{}里,然后去遍历这个数组依次,验证结果。
参考标准库 fmt_test.go里的 fmtTests变量 和 TestSprintf方法。
net/http/httptest
Go提供了
- httptest.NewRequest 模拟一个Request
- httptest.NewRecorder 模拟一个Response
这样就方便我们对一个http.HandlerFunc进行测试
1
| type HandlerFunc func(ResponseWriter, *Request)
|
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| func TestHttp(t *testing.T) {
// 创建一个请求
req := httptest.NewRequest("GET", "http://example.com", nil)
// 创建一个 ResponseRecorder 来记录响应
w := httptest.NewRecorder()
// 创建一个http.HandlerFunc来处理req返回rsp
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"status":true}`)
})
h.ServeHTTP(w, req)
rsp := w.Result()
// 检测状态码
if rsp.StatusCode != http.StatusOK {
t.Errorf("handler return wrong status code: got %v wand %v", rsp.StatusCode, http.StatusOK)
}
// 检测返回值
body, _ := ioutil.ReadAll(rsp.Body)
if string(body) != `{"status":true}` {
t.Errorf("handler return wrong body: got %v wand %v", string(body), `{"status":true}`)
}
}
|
同时httptest.NewServer()提供了可以创建临时的server ,让我们来测试发送HTTP请求响应。如测试http中间件
using an interface to mock results
前提:减少函数间的耦合。
具体做法:通过函数参数 传入接口,间接调用函数。
比如:
- 原先A函数里直接调用了B函数。现在声明一个接口C 包含B函数的签名,就相当于B函数实现了C接口。
- 然后,给A加上 一个函数参数是C类型。
- 这时候 考虑怎么把 函数B传给A。我们再声明一个struct D,让其包含函数B,就相当于D实现了接口C,这时候我们就 可以将D的类型 传入函数A,实现了A间接调用B。
- 这种做法的好处 在于,我们减少了函数间的耦合。建立的更好的抽象之上,适配更多情景。也方便,我们单独的去测试函数A。
设计代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| type ReleasesInfo struct {
Id uint `json:"id"`
TagName string `json:"tag_name"`
}
type ReleaseInfoer interface {
GetLatestReleaseTag(string) (string, error)
}
type GithubReleaseInfoer struct{}
func (g GithubReleaseInfoer) GetLatestReleaseTag(repo string) (string, error) {}
func getReleaseTagMessage(ri ReleaseInfoer, repo string) (string, error) {
tag, err := ri.GetLatestReleaseTag(repo)
if err != nil {
return "", fmt.Errorf("Error querying GitHub API: %s", err)
}
return fmt.Sprintf("The latest release is %s", tag), nil
}
|
测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| type FakeReleaseInfoer struct {
Tag string
Err error
}
func (f FakeReleaseInfoer) GetLatestReleaseTag(repo string) (string, error) {
if f.Err != nil {
return "", f.Err
}
return f.Tag, nil
}
func TestGetReleaseTagMessage(t *testing.T) {
cases := []struct {
f FakeReleaseInfoer
repo string
expectedMsg string
expectedErr error
}{
{
f: FakeReleaseInfoer{
Tag: "v0.1.0",
Err: nil,
},
repo: "doesnt/matter",
expectedMsg: "The latest release is v0.1.0",
expectedErr: nil,
},
{
f: FakeReleaseInfoer{
Tag: "v0.1.0",
Err: errors.New("TCP timeout"),
},
repo: "doesnt/foo",
expectedMsg: "",
expectedErr: errors.New("Error querying GitHub API: TCP timeout"),
},
}
for _, c := range cases {
msg, err := getReleaseTagMessage(c.f, c.repo)
if !reflect.DeepEqual(err, c.expectedErr) {
t.Errorf("Expected err to be %q but it was %q", c.expectedErr, err)
}
if c.expectedMsg != msg {
t.Errorf("Expected %q but got %q", c.expectedMsg, msg)
}
}
}
|
当然上面也可以将函数B作为 函数参数 传递给A。通过声明一个函数类型,然后让B去实现它。
这种做法也减少了 函数间的耦合,就方便测试某一个函数。如我们要测试下面的getTagMsg,我们就可以声明一个me类型的函数,传入getTagMsg。
从而实现了所谓的mock参数,当然它实现的前提是 函数间不能有耦合。即直接的调用。
1
2
3
4
5
6
7
8
9
| type me func(repo string) (string, error)
func getTag(repo string) (string, error) {}
func getTagMsg(m me, repo string) (string, error) {}
func main() {
msg, err := getReleaseTagMessage(getTag, "123")
}
|
总结,上面的两种实现 都要建立在 具体的抽象之上。目的 都要减少函数耦合,方便更好的测试。所以,对待具体情况 要具体看待。
iotest/quick
testing/iotest 用户创建常用的出错的Reader和Writer
testing/quick 用于黑盒测试
testing help function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| func assert(tb testing.TB, condition bool, msg string, v ...interface{}) {
if !condition {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("%s:%d: "+msg+"\n\n", append([]interface{}{filepath.Base(file), line}, v...)...)
tb.FailNow()
}
}
func ok(tb testing.TB, msg string, err error) {
if err != nil {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("%s:%d: %s \n\n unexpected error: %s\n\n", filepath.Base(file), line, msg, err.Error())
tb.FailNow()
}
}
func equals(tb testing.TB, msg string, wat, got interface{}) {
if !reflect.DeepEqual(wat, got) {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("%s:%d: %s \n\n\twat: %#v\n\n\tgot: %#v\n\n", filepath.Base(file), line, msg, wat, got)
tb.FailNow()
}
}
|
测试框架
- Ginkgo is a BDD-style Go testing framework.
- Gomega is a matcher/assertion library. It is best paired with the Ginkgo BDD test framework.
- GoMock is a mocking framework for the Go programming language.
- testify is a toolkit with common assertions and mocks that plays nicely with the standard library.
ginkgo与gomega例子参考
reference