golang测试

测试命名规范

测试文件命名规范

        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)
		}
	}
}

gotests自动生成测试用例

安装

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

golang测试_第1张图片

http测试

待测接口

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 语言单元测试和性能测试用例?-极客时间

你可能感兴趣的:(golang,测试,golang,单元测试)