Go测试文件名必须以_test.go结尾,Go会通过文件名来识别哪些是测试文件,go test的时候会加载这些测试文件,比如有一个待测试文件名为person.go,则测试文件命名为person_test.go
Go测试可分为白盒测试和黑盒测试
测试文件和包的命名规范,由Go语言和go test工具强制约束
测试用例函数必须以Test、Benchmark、Example开头,例如TestXxx、BenchmarkXxx、ExampleXxx,Xxx部分为任意字母数字的组合,首字母大写。这是由 Go 语言和 go test 工具来进行约束的,为了好对应测试函数和待测函数,Xxx一般是待测试的函数名。
单元测试用例通常会有一个实际的输出,在单元测试中,我们会将预期的输出跟实际的输出进行对比,来判断单元测试是否通过。为了清晰地表达函数的实际输出和预期输出,可以将这两类输出命名为expected/actual,或者got/want。
单元测试用例函数以Test开头,函数参数必须是*testing.T,可以使用该类型来记录错误或测试状态。
可以调用 testing.T 的 Error 、Errorf 、FailNow 、Fatal 、FatalIf 方法,来说明测试不通过;调用 Log 、Logf 方法来记录测试信息。
更多可见https://github.com/golang/go/blob/master/src/testing/testing.go#L510
示例为测试grpc中的Hello方法
service.go
package service
import (
"context"
"fmt"
api "grpcl/grpcL/new/api"
"io"
)
type HelloServiceImpl struct{}
//待测方法
func (p *HelloServiceImpl) Hello(ctx context.Context, args *api.String) (*api.String, error) {
reply := &api.String{Value: "hello:" + args.GetValue()}
return reply, nil
}
结构体使用的是grpcl/grpcL/new/api包中api.pb.go的结构体
type String struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
}
测试用例
步骤:构造请求参数->调用函数->获取返回值->比较返回值与期待的返回值
package service
import (
"context"
a "grpcl/grpcL/new/api"
"reflect"
"testing"
)
// 测试
func TestHello(t *testing.T) {
p := &HelloServiceImpl{}
req := &a.String{Value: "req1"}
got, _ := p.Hello(context.Background(), req)
want := &a.String{Value: "hello:req1"}
// fmt.Println(got == want) // false不能直接判等
if !reflect.DeepEqual(got, want){
t.Errorf("excepted:%v, got:%v", want, got)
}
}
// 测试组
func TestHello1(t *testing.T) {
//先声明一个结构体
type tst struct{
req, want *a.String
}
//结构体列表
tsts := []tst{
{req: &a.String{Value: "req1"}, want: &a.String{Value: "hello:req1"}},
{req: &a.String{Value: "req2"}, want: &a.String{Value: "hello:req2"}},
}
p := &HelloServiceImpl{}
for _, r := range tsts{
got, _ := p.Hello(context.Background(), r.req)
if !reflect.DeepEqual(got, r.want){
t.Errorf("excepted:%v, got:%v", r.want, got)
}
}
}
// 子测试
func TestHello2(t *testing.T) {
//先声明一个结构体
type tst struct{
req, want *a.String
}
//使用map
tsts := map[string]tst{
"subTest1": {req: &a.String{Value: "req1"}, want: &a.String{Value: "hello:req1"}},
"subTest2": {req: &a.String{Value: "req2"}, want: &a.String{Value: "hello:req2"}},
}
p := &HelloServiceImpl{}
for subTest, r := range tsts{
got, _ := p.Hello(context.Background(), r.req)
if !reflect.DeepEqual(got, r.want){
t.Errorf("test:%s excepted:%v, got:%v", subTest, r.want, got)
}
}
}
go get -u github.com/cweill/gotests
cd ~/golang/pkg/mod/github.com/cweill/[email protected]/gotests/
go build
cp gotests /usr/local/bin/
待测函数
package tst1
import "strings"
func Split(s, sep string) (result []string) {
i := strings.Index(s, sep)
for i > -1 {
result = append(result, s[:i])
s = s[i+len(sep):]
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
到待测代码目录,.表示为当前目录下所有文件生成测试用例,或者指定文件名
gotests -all -w .
生成的测试用例
package tst1
import (
"reflect"
"testing"
)
func TestSplit(t *testing.T) {
type args struct {
s string
sep string
}
tests := []struct {
name string
args args
wantResult []string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if gotResult := Split(tt.args.s, tt.args.sep); !reflect.DeepEqual(gotResult, tt.wantResult) {
t.Errorf("Split() = %v, want %v", gotResult, tt.wantResult)
}
})
}
}
构造test cases,直接先加上{}
tests := []struct {
name string
args args
wantResult []string
}{
{},
{},
}
然后填入结构体内容
tests := []struct {
name string
args args
wantResult []string
}{
{name: "case1", args: args{s: "a:b:c", sep: ":"}, wantResult: []string{"a", "b", "c"}},
{name: "case1", args: args{s: "abcd", sep: "bc"}, wantResult: []string{"a", "d"}},
}
然后运行测试即可
$ go test .
ok view/tst1 0.001s
-cover参数
$ go test -cover
PASS
coverage: 100.0% of statements
ok view/tst1 0.001s
输出到文件
go test -cover -coverprofile=c.out
生成html文件,并自动在浏览器打开查看测试覆盖情况,绿色部分即表示已覆盖
go tool cover -html=c.out
待测接口
package tst2
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// Param 请求参数
type Param struct {
Name string `json:"name"`
}
// helloHandler /hello请求处理函数
func helloHandler(c *gin.Context) {
var p Param
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(http.StatusOK, gin.H{
"msg": "we need a name",
})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": fmt.Sprintf("hello %s", p.Name),
})
}
// SetupRouter 路由
func SetupRouter() *gin.Engine {
router := gin.Default()
router.POST("/hello", helloHandler)
return router
}
测试用例
package tst2
import (
"encoding/json"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func Test_helloHandler(t *testing.T) {
tests := []struct {
name string
param string
expect string
}{
{name: "case1", param: `{"name": "abc"}`, expect: "hello abc"},
{name: "case2", param: "", expect: "we need a name"},
}
r := SetupRouter()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(
"POST",
"/hello",
strings.NewReader(tt.param),
)
// mock一个响应记录器
w := httptest.NewRecorder()
// 让server端处理mock请求并记录返回的响应内容
r.ServeHTTP(w, req)
// 校验状态码
assert.Equal(t, http.StatusOK, w.Code)
// 获取返回值
var resp map[string]string
err := json.Unmarshal([]byte(w.Body.String()), &resp)
assert.Nil(t, err)
assert.Equal(t, tt.expect, resp["msg"])
})
}
}
在代码中请求外部API的场景,使用gock
对外部API进行mock,即mock指定参数返回约定好的响应内容。
待测函数
package tst3
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
)
// ReqParam API请求参数
type ReqParam struct {
X int `json:"x"`
}
// Result API返回结果
type Result struct {
Value int `json:"value"`
}
func GetResultByAPI(x, y int) int {
p := &ReqParam{X: x}
b, _ := json.Marshal(p)
// 调用其他服务的API
resp, err := http.Post(
"http://your-api.com/post",
"application/json",
bytes.NewBuffer(b),
)
if err != nil {
return -1
}
body, _ := ioutil.ReadAll(resp.Body)
var ret Result
if err := json.Unmarshal(body, &ret); err != nil {
return -1
}
// 这里是对API返回的数据做一些逻辑处理
return ret.Value + y
}
测试用例
package tst3
import (
"gopkg.in/h2non/gock.v1"
"testing"
)
func TestGetResultByAPI(t *testing.T) {
type args struct {
x int
y int
}
tests := []struct {
name string
args args
want int
value int
}{
{"case1", args{x: 1, y: 1}, 101, 100},
{"case2", args{x: 2, y: 2}, 202, 200},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer gock.Off() // 测试执行后刷新挂起的mock
// mock 请求外部api时传参x=1返回100
gock.New("http://your-api.com").
Post("/post").
MatchType("json").
JSON(map[string]int{"x": tt.args.x}). // 传入参数
Reply(200).
JSON(map[string]int{"value": tt.value}) // 返回值
if got := GetResultByAPI(tt.args.x, tt.args.y); got != tt.want {
t.Errorf("GetResultByAPI() = %v, want %v", got, tt.want)
}
})
}
}
使用go-sqlmock,不连接真正的数据库进行测试
待测函数
package tst4
import "database/sql"
// recordStats 记录用户浏览产品信息
func recordStats(db *sql.DB, userID, productID int64) (err error) {
// 开启事务
// 操作views和product_viewers两张表
tx, err := db.Begin()
if err != nil {
return
}
defer func() {
switch err {
case nil:
err = tx.Commit()
default:
tx.Rollback()
}
}()
// 更新products表
if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
return
}
// product_viewers表中插入一条数据
if _, err = tx.Exec(
"INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)",
userID, productID); err != nil {
return
}
return
}
测试用例
package tst4
import (
"fmt"
"testing"
"github.com/DATA-DOG/go-sqlmock"
)
// TestShouldUpdateStats sql执行成功的测试用例
func TestShouldUpdateStats(t *testing.T) {
// mock一个*sql.DB对象,不需要连接真实的数据库
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
// mock执行指定SQL语句时的返回结果
mock.ExpectBegin()
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
// 将mock的DB对象传入我们的函数中
if err = recordStats(db, 2, 3); err != nil {
t.Errorf("error was not expected while updating stats: %s", err)
}
// 确保期望的结果都满足
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
// TestShouldRollbackStatUpdatesOnFailure sql执行失败回滚的测试用例
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
mock.ExpectBegin()
mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("INSERT INTO product_viewers").
WithArgs(2, 3).
WillReturnError(fmt.Errorf("some error"))
mock.ExpectRollback()
// now we execute our method
if err = recordStats(db, 2, 3); err == nil {
t.Errorf("was expecting an error, but there was none")
}
// we make sure that all expectations were met
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
参考以下系列文章
Go单测从零到溜系列—0.单元测试基础
36 | 代码测试(上):如何编写 Go 语言单元测试和性能测试用例?-极客时间