测试类型:
单元测试:
规则:
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"))
})
})