第八章 go单元测试--表格驱动测试, 性能测试
一. go语言单元测试写法
1. 文件命令, 测试内容+ _test
2. 测试的方法名以Test开头. 参数为(test *Test)
3. 测试文件和源文件放在同一个目录中
例:
package TestingDebug
func Add(a, b int) int {
return b + a
}
package TestingDebug
import (
"fmt"
"testing"
func TestAdd(t *testing.T) {
var test = []struct{
a, b, c int
{1, 2, 3},
{3, 5, 8},
{2, 4, 5},
for _, tt := range test {
if actural := Add(tt.a, tt.b) ; actural != tt.c {
fmt.Printf("%d + %d, except:%d, actual:%d", tt.a, tt.b, tt.c, actural)
}
需要注意几点
1. 同目录下文件相互调用, 不需要加包名
2. 测试文件与源文件在同一个目录下, 否则技术不出来测试代码覆盖率.
二. 如何测试代码覆盖率


然后点击TestingDebug进入文件夹, 点击测试的文件. 看右侧绿线, 表示测试覆盖的代码. 这里覆盖了100%

三. 性能测试BenchMark
性能测试使用Testing.B
比如, 我们想要测试一个复杂一些的数据的累加, 看看性能如何.
/**
* 性能测试
func BenchmarkAdd(bb *testing.B) {
var a, b, c int
a = 123
b = 4557
c = 4680
for i := 0; i<bb.N ; i++ {
if actural := Add(a, b) ; actural != c {
fmt.Printf("%d + %d, except:%d, actual:%d", a, b, c, actural)
}
bb.N表示的是系统自动计算的一个循环次数, 我们不用自己指定.
goos: darwin
goarch: amd64
pkg: aaa/TestingDebug
BenchmarkAdd-8 1000000000 0.317 ns/op
PASS
以上是测试结果. 1000000000 代表测试的次数是10亿次. 0.317 ns/op每个操作执行的时间是0.317ns
四. 性能调优--使用pprof进行性能优化
如上一步, 我们对代码进行了性能测试. 每个操作执行时间是0.317ns. 那么, 假如有一个稍微复杂一些的例子, 我如何看出花费了这么多时间, 都是在哪个步骤花的? 哪个步骤耗费时间最多呢?
我们使用命令来看
go test -bench . -cpuprofile cpu.out
执行上面的命令生成一个cpu.out的文件, 查看cpu运行的情况

我们使用less查看文件: less cpu.out

cpu.out是一个二进制文件,我们没办法打开. 我们可以使用命令打开
go tool pprof cpu.out

这里我们可以通过pprof的web视图来查看到底那个步骤耗时最多. 但是看红色字, 查看web视图,需要安装一个软件Graphviz
安装Graphviz, 下载地址: http://www.graphviz.org/ . 我是mac, 下载一个mac版本的:
下载命令: brew install graphviz
安装成功以后, 数据命令 go tool pprof cpu.out , 然后输入web , 会生产一个svg文件, 用浏览器打开我们就会看到效果了

这就是对简单程序的一个分析. 他在每个阶段用时是多少. 因为我们这里是很简单的一个demo, 所以耗时很少了.
五. 测试http 服务器
http测试分为两种
1. 通过使用假的Request/Response实现 : 运行速度更快, 可以测试更多种情况, 更像单元测试.
2. 通过启服务器: 模拟的诊室服务, 覆盖代码更多.

1. 使用假的Request和Response模拟http请求实现单元测试
分析:
首先: 明确我们的目标, 要对谁做测试. 我们要对封装的错误处理进行测试.
第二: 测试有输入和输出. 预计输入输出, 真实输入输出. 我们这里输入是谁呢?通过包装类WrapError分析, 入参是对http请求业务逻辑的处理, 出参是包装的错误处理, 包括错误码和错误消息
所以, 单元测试的表格的结构体怎么写呢?
test := []struct{
h appHandler // 入参
Code int // 出参
Message string // 出参
}
入参是一个appHandler函数. 因此我们定义一个函数,来模拟返回的错误信息. 异常的种类有很多,先定义一个panic
func handlerPanic(writer http.ResponseWriter, request *http.Request) error {
panic("panic error")
}
接下来模拟http请求, 判断异常处理是否正确, 完整代码如下:
package main
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
func handlerPanic(writer http.ResponseWriter, request *http.Request) error {
panic("panic error")
func TestWrapError(t *testing.T) {
// 我们测试的异常的封装方法 那么对于单元测试来说, 无非就是入参和出参
test := []struct{
h appHandler // 入参
Code int // 出参
Message string // 出参
{handlerPanic, 500, "Internal Server Error"},
for _, tt := range test {
// 要测试的方法WrapError
f := WrapError(tt.h)
// 模拟Request和response
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "http://www.baidu.com", nil)
f(response, request)
//读取reponse返回的body
b, _ := ioutil.ReadAll(response.Body)
body := strings.Trim(string(b), "\n")
// 测试返回值和预期是否一致
if tt.Code != response.Code || tt.Message != body {
t.Errorf("预期返回--code:%d, message:%s \n 实际返回--code:%d, message:%s", tt.Code,
tt.Message, response.Code, body)
}
测试用户自定义异常, 我们在测试用户自定义信息
package main
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
//用户自定义异常
type testUserError string
func (u testUserError) Error() string{
return u.Message()
func (u testUserError) Message() string {
return string(u)
func handlerUserError(writer http.ResponseWriter, request *http.Request) error {
return testUserError("user error")
func handlerPanic(writer http.ResponseWriter, request *http.Request) error {
panic("panic error")
func TestWrapError(t *testing.T) {
// 我们测试的异常的封装方法 那么对于单元测试来说, 无非就是入参和出参
test := []struct{
h appHandler // 入参
Code int // 出参
Message string // 出参
{handlerPanic, 500, "Internal Server Error"},
{handlerUserError, 400, "user error"},
for _, tt := range test {
// 要测试的方法WrapError
f := WrapError(tt.h)
// 模拟Request和response
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "http://www.baidu.com", nil)
f(response, request)
//读取reponse返回的body
b, _ := ioutil.ReadAll(response.Body)
body := strings.Trim(string(b), "\n")
// 测试返回值和预期是否一致
if tt.Code != response.Code || tt.Message != body {
t.Errorf("预期返回--code:%d, message:%s \n 实际返回--code:%d, message:%s", tt.Code,
tt.Message, response.Code, body)
}
接下来查看代码覆盖率 35%. 接下来补全测试代码, 提高代码覆盖率
package main
import (
"github.com/pkg/errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
type testUserError string
func (u testUserError) Error() string{
return u.Message()
func (u testUserError) Message() string {
return string(u)
func handlerUserError(writer http.ResponseWriter, request *http.Request) error {
return testUserError("user error")
func handlerPanicError(writer http.ResponseWriter, request *http.Request) error {
panic("panic error")
func handlerNotFountError(writer http.ResponseWriter, request *http.Request) error {
return os.ErrNotExist
func handlerForbiddenError(writer http.ResponseWriter, request *http.Request) error {
return os.ErrPermission
func otherError(writer http.ResponseWriter, request *http.Request) error {
return errors.New("123")
func noError(writer http.ResponseWriter, request *http.Request) error {
return nil
func TestWrapError(t *testing.T) {
// 我们测试的异常的封装方法 那么对于单元测试来说, 无非就是入参和出参
test := []struct{
h appHandler // 入参
Code int // 出参
Message string // 出参
{handlerPanicError, 500, "Internal Server Error"},
{handlerUserError, 400, "user error"},
{handlerNotFountError, 404, "Not Found"},
{handlerForbiddenError, 403, "Forbidden"},
{otherError, 500, "Internal Server Error"},
{noError, 200, ""},
for _, tt := range test {
// 要测试的方法WrapError
f := WrapError(tt.h)
// 模拟Request和response
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "http://www.baidu.com", nil)
f(response, request)
//读取reponse返回的body
b, _ := ioutil.ReadAll(response.Body)
body := strings.Trim(string(b), "\n")
// 测试返回值和预期是否一致
if tt.Code != response.Code || tt.Message != body {
t.Errorf("预期返回--code:%d, message:%s \n 实际返回--code:%d, message:%s", tt.Code,
tt.Message, response.Code, body)
}
再次查看代码覆盖率

达到了80%, 只有最后的main方法没有被覆盖. ok了
2. 使用真实的http请求进行测试
// 真实http请求
func TestWrapErrorForServer(t *testing.T) {
for _, tt := range test {
// 要测试的方法WrapError
f := WrapError(tt.h)
s := httptest.NewServer(http.HandlerFunc(f))
// 这里url不用填自己的, httptest在new server的时候帮我们定义好了一个url
response, _ := s.Client().Get(s.URL)
b, _ := ioutil.ReadAll(response.Body)
body := strings.Trim(string(b), "\n")
if tt.Code != response.StatusCode || tt.Message != body {
t.Errorf("except--code: %d, message: %s \n actual--code:%d, message:%s",
tt.Code, tt.Message, response.StatusCode, body)
}
模拟数据的部分, 两种类型的http请求是一样的, 被提取到外面了, 最终完整代码如下:
package main
import (
"github.com/pkg/errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
type testUserError string
func (u testUserError) Error() string{
return u.Message()
func (u testUserError) Message() string {
return string(u)
func handlerUserError(writer http.ResponseWriter, request *http.Request) error {
return testUserError("user error")
func handlerPanicError(writer http.ResponseWriter, request *http.Request) error {
panic("panic error")
func handlerNotFountError(writer http.ResponseWriter, request *http.Request) error {
return os.ErrNotExist
func handlerForbiddenError(writer http.ResponseWriter, request *http.Request) error {
return os.ErrPermission
func otherError(writer http.ResponseWriter, request *http.Request) error {
return errors.New("123")
func noError(writer http.ResponseWriter, request *http.Request) error {
return nil
// 测试数据
var test = []struct{
h appHandler // 入参
Code int // 出参
Message string // 出参
{handlerPanicError, 500, "Internal Server Error"},
{handlerUserError, 400, "user error"},
{handlerNotFountError, 404, "Not Found"},
{handlerForbiddenError, 403, "Forbidden"},
{otherError, 500, "Internal Server Error"},
{noError, 200, ""},
// 模拟http请求
func TestWrapError(t *testing.T) {
for _, tt := range test {
// 要测试的方法WrapError
f := WrapError(tt.h)
// 模拟Request和response
response := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "http://www.baidu.com", nil)
f(response, request)
//读取reponse返回的body
verify(response.Result(), tt, t)
// 真实http请求
func TestWrapErrorForServer(t *testing.T) {
for _, tt := range test {
// 要测试的方法WrapError
f := WrapError(tt.h)
s := httptest.NewServer(http.HandlerFunc(f))
// 这里url不用填自己的, httptest在new server的时候帮我们定义好了一个url
response, _ := s.Client().Get(s.URL)
verify(response, tt, t)
func verify(response *http.Response, tt struct {h appHandler;Code int;Message string}, t *testing.T) {
b, _ := ioutil.ReadAll(response.Body)
body := strings.Trim(string(b), "\n")
if tt.Code != response.StatusCode || tt.Message != body {
t.Errorf("except--code: %d, message: %s \n actual--code:%d, message:%s",
tt.Code, tt.Message, response.StatusCode, body)
}
package fileListener
import (
"io/ioutil"
"net/http"
"strings"
type UserError string
func (u UserError) Error() string{
return u.Message()
func (u UserError) Message() string {
return string(u)
func FileHandler(writer http.ResponseWriter, request *http.Request) error {
// 获取url路径, 路径是/list/之后的部分
if c := strings.Index(request.URL.Path, "/list/"); c != 0 {
return UserError("url 不是已list开头")
path := request.URL.Path[len("/list/"):]
// 打开文件
file, err := os.Open(path)
if err != nil {
return err
defer file.Close()
// 读出文件
b, err := ioutil.ReadAll(file)
if err != nil {
return err
// 写入文件到页面
writer.Write(b)
return nil
}
package main
import (
"aaa/errorhandling/filelistenerserver/fileListener"
"github.com/kelseyhightower/confd/log"
"net/http"
// 定义一个函数类型的结构, 返回值是erro
type appHandler func(writer http.ResponseWriter, request *http.Request) error
// 封装error
func WrapError(handler appHandler) func (http.ResponseWriter, *http.Request) {
return func(writer http.ResponseWriter, request *http.Request) {
defer func(){
if r := recover(); r != nil {
log.Info("发生异常")
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
// 执行原来的逻辑. 然后增加error的错误处理
err := handler(writer, request)
if err != nil {
if userErr, ok := err.(userError); ok {
http.Error(writer, userErr.Message(), http.StatusBadRequest)
return
code := http.StatusOK
switch {
case os.IsNotExist(err):
code = http.StatusNotFound
case os.IsPermission(err):
code = http.StatusForbidden
default:
code = http.StatusInternalServerError
http.Error(writer, http.StatusText(code), code)
type userError interface {
error // 系统异常
Message() string // 用户自定义异常
// 我们来模拟一个web服务器. 在url上输入一个地址, 然后显示文件内容
// 做一个显示文件的web server
func main() {
http.HandleFunc("/", WrapError(fileListener.FileHandler))