茶葉網(wǎng)站建設(shè)要求百度的營銷策略
測試類型:
單元測試:
規(guī)則:
1.所有測試文件以_test.go結(jié)尾
2.func Testxxx(*testing.T)
3.初始化邏輯放到TestMain中
運(yùn)行:
go test [flags][packages]
Go語言中的測試依賴go test命令。
go test命令是一個(gè)按照一定約定和組織的測試代碼的驅(qū)動(dòng)程序。在包目錄內(nèi),所有以_test.go為后綴名的源代碼文件都是go test測試的一部分,不會(huì)被go build編譯到最終的可執(zhí)行文件中
測試函數(shù):
每個(gè)測試函數(shù)必須導(dǎo)入testing包,測試函數(shù)名必須以Test開頭,測試函數(shù)的基本格式(簽名)如下:
覆蓋率:
顯示代碼覆蓋率的命令
go test [flags][packages] --cover
1.一般覆蓋率:50%~60%,較高覆蓋率80%+
2.測試分支相互獨(dú)立、全面覆蓋
3.測試單元粒度足夠小,函數(shù)單一職責(zé)
依賴:
Mock
1.快速mock函數(shù):
為一個(gè)函數(shù)打樁
為一個(gè)方法打樁
monkey打樁實(shí)例:
代碼實(shí)例:
package split_stringimport ("strings"
)//切割字符串
//example:
//abc,b=>[a c]
func Split(str string, sep string) []string {var ret=make([]string,0,strings.Count(str,sep)+1)//預(yù)先分配好內(nèi)存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){
//測試前:數(shù)據(jù)裝載、配置初始化等前置工作code:=m.Run()//測試后,釋放資源等收尾工作
os.Exist(code)
}
package split_stringimport ("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)}
} //測試用例二//一次測試多個(gè)
func Test3Split(t *testing.T) {type testCase struct {str stringsep stringwant []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模式下會(huì)跳過測試用例")}type testCase struct {str stringsep stringwant []string}testGroup := map[string]testCase{"case 1": testCase{"babcbef", "b", []string{"", "a", "c", "ef"}},"case 2": testCase{"a:b:c", ":", []string{"a", "b", "c"}},"case 3": testCase{"abcdef", "bc", []string{"a", "def"}},"case 4": testCase{"沙河有沙又有河", "有", []string{"沙河", "沙又", "河"}},}for name, test := range testGroup {t.Run(name, func(t *testing.T) {t.Parallel() //將每個(gè)測試用例標(biāo)記為能夠彼此并行運(yùn)行got := Split(test.str, test.sep)if !reflect.DeepEqual(got, test.want) {t.Fatalf("want:%#v got:%#v\n", test.want, got)}})}
} //會(huì)把每個(gè)map中的樣例試結(jié)果都打印出來
func TestMain(m *testing.M) {//測試的入口函數(shù)m.Run()//測試開始
}
//在split_string終端下
go test //進(jìn)行測試
go test -v//查看測試細(xì)節(jié)
go test -cover//語句覆蓋率
go test -cover -coverprofile=cover.out//將測試結(jié)果生成文件
go tool -cover -html=cover.out //生成html文件來分析cover.out,綠色覆蓋,紅色未覆蓋
//好的測試,測試函數(shù)覆蓋率:100% 測試覆蓋率:60%
go test -run=Sep -v //只運(yùn)行測試函數(shù)名中帶有Sep的測試
go test -short //會(huì)跳過某些耗時(shí)的測試用例
基準(zhǔn)測試:
func BenchmarkSplit(b *testing.B){for i:=0;i<b.N;i++{//N不是一個(gè)固定的數(shù),是使Split跑夠1秒鐘的一個(gè)數(shù)Split("a:b:c:d:e",":")}
}
go test -bench=Split
//goos: windows windows平臺(tái)
// goarch: amd64 amd64位
// pkg: test/split_string
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
// BenchmarkSplit-12 12核 3869991 執(zhí)行次數(shù) 307.2 ns/op 307.2ns/次
// PASS
// ok test/split_string 1.539s
go test -bench=Split -benchmem //增加了查看對內(nèi)存的申請情況
網(wǎng)絡(luò)測試:
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))}
}
1.優(yōu)化代碼,需要對當(dāng)前代碼分析
2.內(nèi)置的測試框架提供了基準(zhǔn)測試的能力
func BenchmarkSelect(b *testing.B) {InitServerIndex()b.ResetTimer() //定時(shí)器重置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()}})
}
SqlMock
mock的核心在于屏蔽上游細(xì)節(jié),使用一些實(shí)現(xiàn)設(shè)定好的數(shù)據(jù)來模擬上游返回的數(shù)據(jù)
sqlmock就是在測試過程中,指定你期望(Expectations)執(zhí)行的查詢語句,以及假定的返回結(jié)果(WillReturnResult)
sqlmock庫的安裝:
go get github.com/DATA-DOG/go-sqlmock
sqlmock.New()返回一個(gè)標(biāo)準(zhǔn)的sql.DB結(jié)構(gòu)體實(shí)例指針,這是一個(gè)數(shù)據(jù)庫連接句柄。除此之外還返回了一個(gè)sqlmock.Sqlmock結(jié)構(gòu)體實(shí)例
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為期望執(zhí)行的sql語句//WithArgs代表執(zhí)行該語句所要帶的參數(shù)//sqlmock.AnyArg()代表任意參數(shù)//WillReturnResult里面代表假定的返回//sqlmock.NewResult(1,1)代表自增主鍵為1,1條影響結(jié)果
gorm使用sqlmock測試時(shí)的示例
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 mainimport ("database/sql"_ "github.com/go-sql-driver/mysql"
)type User struct {Id int64Username stringPassword 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 testsdb, 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 mainimport ("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 methodif 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 metif 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 methodif err = recordStats(db, 2, 3); err == nil {t.Errorf("was expecting an error, but there was none")}// we make sure that all expectations were metif 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
1.在引入ginkgo和gomega的時(shí)候前面加.這樣每次調(diào)用時(shí)就可以直接使用包中的方法函數(shù)了。
2.ginkgo使用Describe()來描述這段代碼的行為,使用Context()來描述表達(dá)該行為是在不同的環(huán)境下執(zhí)行,一個(gè)it就是一個(gè)spec即一個(gè)測試用例
3.ginkgo使用BeforeEach()來為specs設(shè)置狀態(tài),并使用It()來指定單個(gè)spec,也是一個(gè)測試用例,且執(zhí)行每一個(gè)It模塊前都會(huì)執(zhí)行一次Describe的BeforeEach和AfterEach,以確保每個(gè)Specs都處于原始狀態(tài)
4.JustBeforeEach()模塊在所有BeforeEach()模塊執(zhí)行之后,It模塊執(zhí)行之前運(yùn)行,BeforeSuit函數(shù)在所有Specs運(yùn)行前執(zhí)行,AfterSuite函數(shù)在所有Specs運(yùn)行后執(zhí)行,不論測試是否失敗
5.使用Gomega中的Expect()函數(shù)來設(shè)置期望
package mainimport ("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.Oncemock sqlmock.Sqlmockdb *sql.DBerr erroruserInfo []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 methoderr = recordStats(db, 2, 3)Expect(err).To(BeNil())// we make sure that all expectations were meterr = 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 methoderr = recordStats(db, 2, 3)Expect(err).To(BeNil())// we make sure that all expectations were meterr := 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 mainimport ("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 mainimport ("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.Oncemock redismock.ClientMockredisClient *redis.Client)BeforeEach(func() {once.Do(func() {redisClient, mock = redismock.NewClientMock()})})Context("test set", func() {id := 123key := 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語言非常常用的一個(gè)打樁工具,它在運(yùn)行時(shí)通過匯編語言重寫可執(zhí)行文件,將目標(biāo)函數(shù)或方法的實(shí)現(xiàn)跳轉(zhuǎn)到樁實(shí)現(xiàn)。
monkey庫很強(qiáng)大,但是使用時(shí)需注意以下事項(xiàng):
1.monkey不支持內(nèi)聯(lián)函數(shù),在測試的時(shí)候需要通過命令行參數(shù)-gcflags=-1關(guān)閉Go語言的內(nèi)聯(lián)優(yōu)化
2.monkey不是線程安全的,所以不能把它放到并發(fā)的單元測試中
安裝monkey庫的命令:
go get bou.ke/monkey
簡單的monkey打樁示例:
main.go
package mainimport ("fmt"
)type Student struct {
}func (S *Student) GetInfoByUID(id int64) (string, error) {return "", nil
}func MyFunc(uid int64) string {var varys Studentu, err := varys.GetInfoByUID(uid)if err != nil {return "welcome"}// 這里是一些邏輯代碼...return fmt.Sprintf("hello %s\n", u)
}
main_test.go
package mainimport ("bou.ke/monkey". "github.com/onsi/ginkgo". "github.com/onsi/gomega"
)var _ = Describe("Record Test", func() {Context("test myfunc", func() {var varys Studentmonkey.Patch(varys.GetInfoByUID, func(num int64) (string, error) {return "zyj", nil})ret := MyFunc(123)Expect(ret).NotTo(Equal("welcome"))})})