golang入门笔记——测试

测试类型:

在这里插入图片描述

单元测试:

在这里插入图片描述

规则:

1.所有测试文件以_test.go结尾
2.func Testxxx(*testing.T)
3.初始化逻辑放到TestMain中

运行:

go test [flags][packages]

Go语言中的测试依赖go test命令。
go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中
测试函数:
每个测试函数必须导入testing包,测试函数名必须以Test开头,测试函数的基本格式(签名)如下:
在这里插入图片描述

覆盖率:

显示代码覆盖率的命令

go test [flags][packages] --cover

1.一般覆盖率:50%~60%,较高覆盖率80%+
2.测试分支相互独立、全面覆盖
3.测试单元粒度足够小,函数单一职责

依赖:

在这里插入图片描述

Mock

1.快速mock函数:
为一个函数打桩
为一个方法打桩

monkey打桩实例:
在这里插入图片描述

代码实例:

package split_string

import (
	"strings"
)

//切割字符串
//example:
//abc,b=>[a c]
func Split(str string, sep string) []string {
	var ret=make([]string,0,strings.Count(str,sep)+1)//预先分配好内存
	index := strings.Index(str, sep)
	for index >= 0 {
		ret = append(ret, str[:index])
		str = str[index+len(sep):]
		index = strings.Index(str, sep)
	}
	if str != "" {
		ret = append(ret, str)
	}
	return ret
}

func TestMain(m *testing.M){
//测试前:数据装载、配置初始化等前置工作

code:=m.Run()

//测试后,释放资源等收尾工作
os.Exist(code)
}
package split_string

import (
	"reflect"
	"testing"
)

func TestSplit(t *testing.T) {
	ret := Split("babcbef", "b")
	want := []string{"", "a", "c", "ef"}
	if !reflect.DeepEqual(ret, want) {
		//测试用例失败了
		t.Errorf("want:%v but got:%v\n", want, ret)
	}
} //测试用例一

func Test2Split(t *testing.T) {
	ret := Split("a:b:c", ":")
	want := []string{"a", "b", "c"}
	if !reflect.DeepEqual(ret, want) {
		t.Fatalf("want:%v but get:%v\n", want, ret)
	}
} //测试用例二

//一次测试多个
func Test3Split(t *testing.T) {
	type testCase struct {
		str  string
		sep  string
		want []string
	}
	testGroup := []testCase{
		testCase{"babcbef", "b", []string{"", "a", "c", "ef"}},
		testCase{"a:b:c", ":", []string{"a", "b", "c"}},
		testCase{"abcdef", "bc", []string{"a", "def"}},
		testCase{"沙河有沙又有河", "有", []string{"沙河", "沙又", "河"}},
	}
	for _, test := range testGroup {
		got := Split(test.str, test.sep)
		if !reflect.DeepEqual(got, test.want) {
			t.Fatalf("want:%#v got:%#v\n", test.want, got)
		}
	}
}

//子测试
func Test4Split(t *testing.T) {
	if testing.Short(){
		t.Skip("short模式下会跳过测试用例")
	}
	type testCase struct {
		str  string
		sep  string
		want []string
	}
	testGroup := map[string]testCase{
		"case1": testCase{"babcbef", "b", []string{"", "a", "c", "ef"}},
		"case2": testCase{"a:b:c", ":", []string{"a", "b", "c"}},
		"case3": testCase{"abcdef", "bc", []string{"a", "def"}},
		"case4": testCase{"沙河有沙又有河", "有", []string{"沙河", "沙又", "河"}},
	}

	for name, test := range testGroup {
		t.Run(name, func(t *testing.T) {
			t.Parallel()	//将每个测试用例标记为能够彼此并行运行
			got := Split(test.str, test.sep)
			if !reflect.DeepEqual(got, test.want) {
				t.Fatalf("want:%#v got:%#v\n", test.want, got)
			}
		})
	}
} //会把每个map中的样例试结果都打印出来
func TestMain(m *testing.M) {//测试的入口函数
	m.Run()//测试开始
}

go test -run Test4Split/case1

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

//在split_string终端下
go test //进行测试
go test -v//查看测试细节
go test -cover//语句覆盖率
go test -cover -coverprofile=cover.out//将测试结果生成文件
go tool -cover -html=cover.out  //生成html文件来分析cover.out,绿色覆盖,红色未覆盖
//好的测试,测试函数覆盖率:100%  测试覆盖率:60%
go test -run=Sep -v  //只运行测试函数名中带有Sep的测试
go test -short //会跳过某些耗时的测试用例

基准测试:

func BenchmarkSplit(b *testing.B){
	for i:=0;i<b.N;i++{//N不是一个固定的数,是使Split跑够1秒钟的一个数
		Split("a:b:c:d:e",":")
	}
}
go test -bench=Split
//goos: windows windows平台
// goarch: amd64  amd64位
// pkg: test/split_string
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
// BenchmarkSplit-12  12核   3869991    执行次数      307.2 ns/op 307.2ns/次
// PASS
// ok      test/split_string       1.539s
go test -bench=Split -benchmem //增加了查看对内存的申请情况

1.优化代码,需要对当前代码分析
2.内置的测试框架提供了基准测试的能力

func BenchmarkSelect(b *testing.B) {
	InitServerIndex()
	b.ResetTimer() //定时器重置
	for i := 0; i < b.N; i++ {
		Select()
	}
}

func BenchmarkSelectParallel(b *testing.B) {
	InitServerIndex()
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			Select()
		}
	})
}

并行用法:RunParallel并行的执行benchmark。RunParallel创建p个groutine然后把b.N个迭代测试分布到这些goroutine上
groutine的数目默认是GOMAXPROCS。

网络测试:

func TestHelloHandler(t *testing.T) {
	ln, err := net.Listen("tcp", "localhost:0")
	handleError(t, err)
	defer ln.Close()

	http.HandleFunc("/hello", HelloHandler)
	go http.Serve(ln, nil)

	resp, err := http.Get("http://" + ln.Addr().String() + "hello")
	handleError(t, err)

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	handleError(t, err)

	if string(body) != "hello world!" {
		t.Fatal("expected hello world,but got", string(body))
	}
}
func HelloHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello world"))
}
func TestConn(t *testing.T) {
	ln, err := net.Listen("tcp", "127.0.0.1:8000")
	if err != nil {
		panic(err)
	}
	defer ln.Close()
	http.HandleFunc("/hello", base.HelloHandler)
	go http.Serve(ln, nil)
	resp, err := http.Get("http://" + ln.Addr().String() + "/hello")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	if string(body) != "hello world" {
		t.Fatalf("expect hello world but got %#v", string(body))
	}
}

针对 http 开发的场景,使用标准库 net/http/httptest 对http进行mock来测试更为高效。

func TestConn(t *testing.T) {

	ts := httptest.NewServer(http.HandlerFunc(base.HelloHandler))
	defer ts.Close()
	client := ts.Client()
	resp, err := client.PostForm(ts.URL+"/person", url.Values{
		"addr": []string{"shanghai"},
	})
	if err != nil {
		t.Fatalf("get from url err:%#v", err)
	}
	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)
	if string(body) != "hello world" {
		t.Fatalf("want:hello world got:%#v", string(body))
	}
	fmt.Println(string(body))
}

package base_test

import (
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
)

func TestConn(t *testing.T) {

	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("hello world"))
		if r.Method != "POST" {
			t.Fatalf("Expected 'GET' request, got %#v", r.Method)
		}
		if r.URL.EscapedPath() != "/person" {
			t.Fatalf("Expected request to '/person',got %s", r.URL.EscapedPath())
		}
		//r.ParseForm()
		//topic := r.Form.Get("addr")
		//if topic != "shanghai" {
		//	t.Fatalf("Expected request to have 'addr=shanghai', got:'%s'", topic)
		//}
		w.WriteHeader(http.StatusOK)
	}))
	defer ts.Close()
	client := ts.Client()
	resp, err := client.PostForm(ts.URL+"/person", url.Values{
		"addr": []string{"shanghai"},
	})
	if err != nil {
		t.Fatalf("get from url err:%#v", err)
	}
	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)
	if string(body) != "hello world" {
		t.Fatalf("want:hello world got:%#v", string(body))
	}
}

SqlMock

mock的核心在于屏蔽上游细节,使用一些实现设定好的数据来模拟上游返回的数据
sqlmock就是在测试过程中,指定你期望(Expectations)执行的查询语句,以及假定的返回结果(WillReturnResult)

sqlmock库的安装:

go get github.com/DATA-DOG/go-sqlmock

sqlmock.New()返回一个标准的sql.DB结构体实例指针,这是一个数据库连接句柄。除此之外还返回了一个sqlmock.Sqlmock结构体实例

	db, mock, err := sqlmock.New()
	if err != nil {
		t.Errorf("create sqlmock fail")
	}
	defer db.Close()
	mock.ExpectExec(`sql sentence`).WithArgs(sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1))
	//ExpectExec里的sql sentence为期望执行的sql语句
	//WithArgs代表执行该语句所要带的参数
	//sqlmock.AnyArg()代表任意参数
	//WillReturnResult里面代表假定的返回
	//sqlmock.NewResult(1,1)代表自增主键为1,1条影响结果

gorm使用sqlmock测试时的示例

db, mock, err := sqlmock.New()
if nil != err {
    t.Fatalf("Init sqlmock failed, err %v", err)
}
defer db.Close()

gormDB, err := gorm.Open(mysql.New(mysql.Config{
    SkipInitializeWithVersion: true,
    Conn: db,
    }), &gorm.Config{})
if err!=nil {
    t.Fatalf("Init DB with sqlmock failed, err %v", err)
} else {
    if db, err := database.KnodiDB.DB(); nil != err {
        db.SetMaxIdleConns(10)
        db.SetMaxOpenConns(10)
        db.Ping()
    }
}

增删改查 sqlmock示例

main.go

package main

import (
	"database/sql"

	_ "github.com/go-sql-driver/mysql"
)

type User struct {
	Id       int64
	Username string
	Password string
}

func recordStats(db *sql.DB, userID, productID int64) (err error) {
	tx, err := db.Begin()
	if err != nil {
		return
	}

	defer func() {
		switch err {
		case nil:
			err = tx.Commit()
		default:
			tx.Rollback()
		}
	}()

	if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
		return
	}
	if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil {
		return
	}
	return
}

func recordQuery(db *sql.DB, userID int64) (user *User, err error) {
	row := db.QueryRow("SELECT * FROM user where id =?", userID)
	user = new(User)
	err = row.Scan(&user.Id, &user.Username, &user.Password)
	return
}

func recordDelete(db *sql.DB, userID int64) (err error) {
	_, err = db.Exec("DELETE FROM user where id=?", userID)
	if err != nil {
		return
	}
	return
}

func main() {
	// @NOTE: the real connection is not required for tests
	db, err := sql.Open("mysql", "root@/blog")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil {
		panic(err)
	}
}

main_test.go

package main

import (
	"fmt"
	"regexp"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
)

// a successful case
func TestShouldUpdateStats(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).WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	// now we execute our method
	if err = recordStats(db, 2, 3); err != nil {
		t.Errorf("error was not expected while updating stats: %s", err)
	}

	// we make sure that all expectations were met
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("there were unfulfilled expectations: %s", err)
	}
}

// a failing test case
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)
	}
}

func TestQuery(t *testing.T) {
	userInfo := []string{
		"id",
		"username",
		"password",
	}
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
	}
	mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM user")).WithArgs(1).WillReturnRows(sqlmock.NewRows(userInfo).AddRow(1, "zyj", "123"))
	if err = recordStats(db, 2, 3); err == nil {
		t.Errorf("was expecting an error, but there was none")
	}
	user, err := recordQuery(db, 1)
	if err != nil || user == nil {
		t.Errorf("this is something wrong")
	}
	t.Log(user)
}

func TestDelete(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)
	}
	mock.ExpectExec("DELETE FROM user").WithArgs(1).WillReturnResult(sqlmock.NewResult(1, 1))

	err = recordDelete(db, 1)
	if err != nil {
		t.Errorf("this is something wrong")
	}
}

ginkgo和gomega

BDD(Behavior-driven development)测试架构:行为驱动测试。是对TDD(Test driven development)的改进,BDD可以让开发聚集在模型的行为上。

1.在引入ginkgo和gomega的时候前面加.这样每次调用时就可以直接使用包中的方法函数了。
2.ginkgo使用Describe()来描述这段代码的行为,使用Context()来描述表达该行为是在不同的环境下执行,一个it就是一个spec即一个测试用例
3.ginkgo使用BeforeEach()来为specs设置状态,并使用It()来指定单个spec,也是一个测试用例,且执行每一个It模块前都会执行一次Describe的BeforeEach和AfterEach,以确保每个Specs都处于原始状态
4.JustBeforeEach()模块在所有BeforeEach()模块执行之后,It模块执行之前运行,BeforeSuit函数在所有Specs运行前执行,AfterSuite函数在所有Specs运行后执行,不论测试是否失败
5.使用Gomega中的Expect()函数来设置期望

package main

import (
	"database/sql"
	"fmt"
	"regexp"
	"sync"

	"github.com/DATA-DOG/go-sqlmock"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Record Test", func() {
	var (
		once     sync.Once
		mock     sqlmock.Sqlmock
		db       *sql.DB
		err      error
		userInfo []string
	)
	BeforeEach(func() {
		once.Do(func() {
			db, mock, err = sqlmock.New()
			if err != nil {
				fmt.Println(err)
			}
		})
		userInfo = []string{
			"id",
			"username",
			"password",
		}
	})
	Context("test update and insert", func() {
		It("text update and insert success", func() {
			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()

			// now we execute our method
			err = recordStats(db, 2, 3)
			Expect(err).To(BeNil())

			// we make sure that all expectations were met
			err = mock.ExpectationsWereMet()
			Expect(err).To(BeNil())
		})
		It("text update and insert failed", func() {
			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
			err = recordStats(db, 2, 3)
			Expect(err).To(BeNil())
			// we make sure that all expectations were met
			err := mock.ExpectationsWereMet()
			Expect(err).To(BeNil())
		})
		Context("test query", func() {
			It("test query", func() {
				mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM user")).WithArgs(1).WillReturnRows(sqlmock.NewRows(userInfo).AddRow(1, "zyj", "123"))
				err = recordStats(db, 2, 3)
				Expect(err).To(BeNil())
				user, err := recordQuery(db, 1)
				Expect(err).To(BeNil())
				Expect(user.Id).To(Equal(int64(1)))
			})
		})
		Context("text delete", func() {
			It("test delete", func() {
				mock.ExpectExec("DELETE FROM user").WithArgs(1).WillReturnResult(sqlmock.NewResult(1, 1))

				err = recordDelete(db, 1)
				Expect(err).To(BeNil())
			})
		})
	})
})

RedisMock

redismock的安装:

go get github.com/go-redis/redismock/v9

简单示例:

main.go

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

func NewsInfoForCache(redisDB *redis.Client, newsID int) (info string, err error) {
	cacheKey := fmt.Sprintf("news_redis_cache_%d", newsID)
	ctx := context.TODO()
	info, err = redisDB.Get(ctx, cacheKey).Result()
	if err == redis.Nil {
		// info, err = call api()
		info = "test"
		err = redisDB.Set(ctx, cacheKey, info, 30*time.Minute).Err()
	}
	return
}

main_test.go

package main

import (
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/go-redis/redismock/v9"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"github.com/redis/go-redis/v9"
)

var _ = Describe("Record Test", func() {
	var (
		once        sync.Once
		mock        redismock.ClientMock
		redisClient *redis.Client
	)
	BeforeEach(func() {
		once.Do(func() {
			redisClient, mock = redismock.NewClientMock()
		})
	})

	Context("test set", func() {
		id := 123
		key := fmt.Sprintf("new_redis_cache_%d", id)
		It("test set success", func() {
			mock.ExpectGet(key).RedisNil()
			mock.Regexp().ExpectSet(key, `[a-z]+`, 30*time.Second).SetErr(errors.New("FAIL"))
			_, err := NewsInfoForCache(redisClient, id)
			Expect(err).To(BeNil())
		})
	})

})

Monkey

monkey是go语言非常常用的一个打桩工具,它在运行时通过汇编语言重写可执行文件,将目标函数或方法的实现跳转到桩实现。

monkey库很强大,但是使用时需注意以下事项:

1.monkey不支持内联函数,在测试的时候需要通过命令行参数-gcflags=-1关闭Go语言的内联优化
2.monkey不是线程安全的,所以不能把它放到并发的单元测试中

安装monkey库的命令:

go get bou.ke/monkey

简单的monkey打桩示例:

main.go

package main

import (
	"fmt"
)

type Student struct {
}

func (S *Student) GetInfoByUID(id int64) (string, error) {
	return "", nil
}

func MyFunc(uid int64) string {
	var varys Student
	u, err := varys.GetInfoByUID(uid)
	if err != nil {
		return "welcome"
	}

	// 这里是一些逻辑代码...

	return fmt.Sprintf("hello %s\n", u)
}

main_test.go

package main

import (
	"bou.ke/monkey"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
)

var _ = Describe("Record Test", func() {

	Context("test myfunc", func() {
		var varys Student
		monkey.Patch(varys.GetInfoByUID, func(num int64) (string, error) {
			return "zyj", nil
		})
			// 为对象方法打桩
	monkey.PatchInstanceMethod(reflect.TypeOf(varys), "CalcAge", func(*User)int {
		return 18
	})
		ret := MyFunc(123)
		Expect(ret).NotTo(Equal("welcome"))
	})

})

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值