Contents

Go Testing Tips

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

前提:减少函数间的耦合。

具体做法:通过函数参数 传入接口,间接调用函数。

比如:

  1. 原先A函数里直接调用了B函数。现在声明一个接口C 包含B函数的签名,就相当于B函数实现了C接口。
  2. 然后,给A加上 一个函数参数是C类型。
  3. 这时候 考虑怎么把 函数B传给A。我们再声明一个struct D,让其包含函数B,就相当于D实现了接口C,这时候我们就 可以将D的类型 传入函数A,实现了A间接调用B。
  4. 这种做法的好处 在于,我们减少了函数间的耦合。建立的更好的抽象之上,适配更多情景。也方便,我们单独的去测试函数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