目錄
什么是 mock
變量打樁
接口方法/Redis
函數(shù)/方法打樁
包函數(shù)
成員方法
MySQL
sqlmock
sqlite mock gorm
http mock
源碼地址
單測基礎(chǔ)
什么是 mock
? ? ? ?單元測試,顧名思義對某個單元函數(shù)進(jìn)行測試,被測函數(shù)本身中用到的變量、函數(shù)、資源不應(yīng)被測試代碼依賴,所謂 mock,就是想辦法通過 “虛擬” 代碼替換掉依賴的方法和資源,一般需要 mock 掉以下依賴:
-
變量
-
函數(shù)/方法
-
MySQL
-
Redis
-
http 調(diào)用
變量打樁
有時我們的代碼里依賴一個全局變量,測試方法根據(jù)全局變量的不同值執(zhí)行不同的邏輯,那么可以用 gostub?對變量進(jìn)行打樁。
?global.go:
package main
var size = 5
func Size() int {
if size > 10 {
return 10
}
return size
}
package main
import (
"testing"
"github.com/agiledragon/gomonkey/v2"
"github.com/prashantv/gostub"
)
func TestSizeStub(t *testing.T) {
tests := []struct {
name string
want int
f func() *gostub.Stubs
}{
{name: "size > 10", want: 10, f: func() *gostub.Stubs {
return gostub.Stub(&size, 11)
}},
{name: "size <= 10", want: 3, f: func() *gostub.Stubs {
return gostub.Stub(&size, 3)
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stub := tt.f()
if got := Size(); got != tt.want {
t.Errorf("Size() = %v, want %v", got, tt.want)
}
stub.Reset()
})
}
}
func TestSizeMonkey(t *testing.T) {
tests := []struct {
name string
want int
f func() *gomonkey.Patches
}{
{name: "size > 10", want: 10, f: func() *gomonkey.Patches {
return gomonkey.ApplyGlobalVar(&size, 11)
}},
{name: "size <= 10", want: 3, f: func() *gomonkey.Patches {
return gomonkey.ApplyGlobalVar(&size, 3)
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stub := tt.f()
if got := Size(); got != tt.want {
t.Errorf("Size() = %v, want %v", got, tt.want)
}
stub.Reset()
})
}
}
$ go test -v -cover
=== RUN TestSize
=== RUN TestSize/size_>_10
=== RUN TestSize/size_<=_10
--- PASS: TestSize (0.00s)
--- PASS: TestSize/size_>_10 (0.00s)
--- PASS: TestSize/size_<=_10 (0.00s)
PASS
coverage: 100.0% of statements
接口方法/Redis
首先 Go 語言推薦的是面向接口編程,所以官方提供并推薦使用? gomock? 對依賴的方法進(jìn)行 mock,前提是依賴的方法是通過抽象接口實現(xiàn)的,gomock 執(zhí)行過程如下:
-
使用mockgen為你想要mock的接口生成一個mock。
-
在你的測試代碼中,創(chuàng)建一個gomock.Controller實例并把它作為參數(shù)傳遞給mock對象的構(gòu)造函數(shù)來創(chuàng)建一個mock對象。
-
調(diào)用EXPECT()為你的mock對象設(shè)置各種期望和返回值。
-
調(diào)用mock控制器的Finish()以驗證mock的期望行為。
gomock 常用方法:
類型 |
用法 |
作用 |
參數(shù) |
gomock.Any(v) |
匹配任何類型 |
gomock.Eq(v) |
匹配使用反射?reflect.DeepEqual 與 v 相等的值 |
|
gomock.Not(v) |
v 不是 Matcher 時,匹配使用反射?reflect.DeepEqual 與 v 不相等的值;v 是 Matcher 時,匹配和 Macher 不匹配的值(Matcher) |
|
gomock.Nil() |
匹配等于 nil 的值 |
|
返回 |
Return() |
mock 方法返回值 |
Do(func) |
傳入的 func 在 mock 真正被調(diào)用時自動執(zhí)行,忽略 Return,比如:對調(diào)用方法的參數(shù)進(jìn)行校驗 |
|
DoAndReturn(func) |
傳入的 func 在 mock 真正被調(diào)用時自動執(zhí)行,對應(yīng) func 返回值作為 mock 方法返回值 |
|
調(diào)用次數(shù) |
AnyTimes(n int) |
mock 方法可以被調(diào)用任意次數(shù),一次不調(diào)用也不會失敗(這里大家可以自檢一下各自的單測代碼,用這個方法的單測可能并沒有按照預(yù)期運行) |
Times() |
mock 方法被調(diào)用次數(shù),次數(shù)不相等運行失敗 |
|
MaxTimes(n int) |
mock 方法被調(diào)用次數(shù),大于規(guī)定次數(shù)運行失敗 |
|
MinTimes(n int) |
mock 方法被調(diào)用次數(shù),小于規(guī)定次數(shù)運行失敗 |
|
調(diào)用排序 |
gomock.InOrder( first.EXPECT.Func().Return(), second.EXPECT.Func().Return(), thrid.EXPECT.Func().Return(), ) |
規(guī)定多個 mock 方法的調(diào)用順序,順序不符運行失敗 |
first := rc.EXPECT().DoFucn() second := rc.EXPECT().DoFunc().After(first) |
規(guī)定多個 mock 方法的先后依賴關(guān)系,順序不符運行失敗 |
首先通過 mockgen 生成 Redis Client 的 mock 代碼:
$ go get -u github.com/golang/mock/gomock
$ go install github.com/golang/mock/mockgen
本地interface:
mockgen[go run -mod=mod github.com/golang/mock/mockgen -package mock] -source ~/go/pkg/mod/github.com/opentracing/opentracing-go\@v1.2.0/tracer.go -destination ./opentracing/tracer.go?Tracer
遠(yuǎn)端interface:
go run -mod=mod github.com/golang/mock/mockgen -package redis -destination ./mock/redis/redis.go github.com/go-redis/redis/v8 Cmdable
redis.go:
package main
import (
"context"
"github.com/go-redis/redis/v8"
)
func handleRedis(c redis.Cmdable) (string, error) {
return c.Get(context.Background(), "redis").Result()
}
func conn() *redis.Client {
return redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
}
package main
import (
"testing"
"github.com/go-redis/redis/v8"
"github.com/golang/mock/gomock"
)
func Test_handleRedis(t *testing.T) {
ctl := gomock.NewController(t)
defer ctl.Finish()
c := NewMockCmdable(ctl)
c.EXPECT().Get(gomock.Any(), gomock.Any()).Times(1).Return(redis.NewStringResult("redis", nil))
handleRedis(c)
}
函數(shù)/方法打樁
假如我們依賴的其他人寫的方法,并不是通過接口實現(xiàn)的,無法使用 gomock 時,可以用 gomonkey 進(jìn)行打樁
包函數(shù)
常用函數(shù):
-
gomonkey.ApplyFunc():單個包函數(shù)打樁
-
gomonkey.ApplyFuncSeq():連續(xù)多個包函數(shù)打樁
func.go
package main
func A() int {
return B()
}
func AA() int {
return B() + B()
}
func B() int {
return 0
}
package main
import (
"testing"
"github.com/agiledragon/gomonkey/v2"
"github.com/stretchr/testify/assert"
)
// TestA 函數(shù),單次打樁
func TestA(t *testing.T) {
patch := gomonkey.ApplyFunc(B, func() int {
return 1
})
defer patch.Reset()
assert.Equal(t, 1, A())
}
// TestAA 函數(shù),連續(xù)打樁
func TestAA(t *testing.T) {
patch := gomonkey.ApplyFuncSeq(B, []gomonkey.OutputCell{
{Values: gomonkey.Params{1}},
{Values: gomonkey.Params{2}},
})
defer patch.Reset()
assert.Equal(t, 3, AA())
}
成員方法
常用函數(shù):
-
gomonkey.ApplyMethod():單個公有成員方法打樁
-
patch.ApplyPrivateMethod():單個私有成員方法打樁
-
patch.ApplyMethodSeq():連續(xù)多個公有成員方法打樁
-
gomonkey.ApplyFuncSeq():連續(xù)多個私有成員方法打樁
method.go
package main
type S struct{}
func (s *S) A() int {
return s.B() + s.b()
}
func (s *S) AA() int {
return s.B() + s.b() + s.B() + s.b()
}
func (s *S) B() int {
return 0
}
func (s *S) b() int {
return 0
}
package main
import (
"reflect"
"testing"
"github.com/agiledragon/gomonkey/v2"
"github.com/stretchr/testify/assert"
)
// TestS_AA 成員方法單個打樁
func TestS_A(t *testing.T) {
s := &S{}
// 公共成員方法
patch := gomonkey.ApplyMethod(reflect.TypeOf(s), "B", func(_ *S) int {
return 1
})
// 私有成員方法
patch.ApplyPrivateMethod(reflect.TypeOf(s), "b", func(_ *S) int {
return 2
})
defer patch.Reset()
assert.Equal(t, 3, s.A())
}
// TestS_AA 成員方法連續(xù)打樁
func TestS_AA(t *testing.T) {
s := &S{}
// 私有成員方法
patch := gomonkey.ApplyFuncSeq((*S).b, []gomonkey.OutputCell{
{Values: gomonkey.Params{1}},
{Values: gomonkey.Params{2}},
})
// 公共成員方法
patch.ApplyMethodSeq(reflect.TypeOf(s), "B", []gomonkey.OutputCell{
{Values: gomonkey.Params{1}},
{Values: gomonkey.Params{2}},
})
defer patch.Reset()
assert.Equal(t, 6, s.AA())
}
MySQL
sqlmock
db.go
package main
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
const dsn = "root:123456@tcp(127.0.0.1:3306)/test"
type Test struct {
ID int64 `json:"id" db:"id" gorm:"column:id"`
GoodsID int64 `json:"goodsID" db:"goods_id" gorm:"column:goods_id"`
Name string `json:"name" db:"name" gorm:"column:name"`
}
func (Test) TableName() string {
return "test"
}
func handle(db *sql.DB) (err error) {
tx, err := db.Begin()
if err != nil {
return
}
defer func() {
switch err {
case nil:
err = tx.Commit()
default:
tx.Rollback()
}
}()
rows, err := tx.Query("SELECT * from test where id > ?", 0)
if err != nil {
panic(err)
}
result := []Test{}
if err = sqlx.StructScan(rows, &result); err != nil {
panic(err)
}
b, err := json.Marshal(result)
if err != nil {
panic(err)
}
fmt.Println("sql:", string(b))
if _, err = tx.Exec("UPDATE test SET goods_id = goods_id + 1 where id = 2"); err != nil {
return
}
if _, err = tx.Exec("INSERT INTO test (goods_id, name) VALUES (?, ?)", 1, "1"); err != nil {
return
}
return
}
func main() {
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
defer db.Close()
if err = handle(db); err != nil {
panic(err)
}
}
package main
import (
"log"
"os"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
_ "github.com/go-sql-driver/mysql"
"github.com/stretchr/testify/assert"
)
func Test_handle(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
panic(err)
}
mock.ExpectBegin()
// (.+) 用于替代字段,可用于 select、order、group等
mock.ExpectQuery("SELECT (.+) from test where id > ?").WillReturnRows(sqlmock.NewRows([]string{"id", "goods_id", "name"}).AddRow(1, 1, "1"))
// sql前綴匹配
mock.ExpectExec("UPDATE test SET goods_id").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("INSERT INTO test").WithArgs(1, "1").WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
if err = handle(db); err != nil {
panic(err)
}
if err = mock.ExpectationsWereMet(); err != nil {
panic(err)
}
}
sqlite mock gorm
如果遇到如下錯誤:
/usr/local/go16/pkg/tool/linux_amd64/link: running gcc failed: exit status 1
/usr/bin/ld: /tmp/go-link-866330658/000020.o(.text+0x74): unresolvable H??@?>H??FH??H??H??@?~?F?H??@?~H??8?H??H??0?FH??H??(?FH??H?? ?FH??H???FH??H???FH??H??F?fD relocation against symbol `stderr@@GLIBC_2.2.5'
/usr/bin/ld: BFD version 2.20.51.0.2-5.34.el6 20100205 internal error, aborting at reloc.c line 443 in bfd_get_reloc_size
/usr/bin/ld: Please report this bug.
collect2: ld returned 1 exit status
更新 go env gcc 版本:
go env -w CC=/opt/compiler/gcc-8.2/bin/gcc
go env -w CXX=/opt/compiler/gcc-8.2/bin/g++
或
CC=/opt/compiler/gcc-8.2/bin/gcc CXX=/opt/compiler/gcc-8.2/bin/g++ go test -c -cover
db.go文章來源:http://www.zghlxwxcb.cn/news/detail-787909.html
package main
import (
"database/sql"
"encoding/json"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
const dsn = "root:123456@tcp(127.0.0.1:3306)/test"
type Test struct {
ID int64 `json:"id" db:"id" gorm:"column:id"`
GoodsID int64 `json:"goodsID" db:"goods_id" gorm:"column:goods_id"`
Name string `json:"name" db:"name" gorm:"column:name"`
}
func (Test) TableName() string {
return "test"
}
func main() {
orm, err := gorm.Open(mysql.Open(dsn))
if err != nil {
panic(err)
}
handleOrm(orm)
}
func handleOrm(orm *gorm.DB) {
var rows []Test
clause := func(db *gorm.DB) *gorm.DB {
return db.Where("id >= ?", 1)
}
err := clause(orm.Select("*")).Find(&rows).Error
if err != nil {
panic(err)
}
b, err := json.Marshal(rows)
if err != nil {
panic(err)
}
fmt.Println("gorm", string(b))
}
package main
import (
"log"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func Test_handleOrm(t *testing.T) {
db := NewMemoryDB()
err := db.Migrator().CreateTable(&Test{})
assert.Nil(t, err)
handleOrm(db)
}
func NewMemoryDB() *gorm.DB {
var db *gorm.DB
var err error
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 閾值
LogLevel: logger.Info, // Log level
Colorful: false, // 禁用彩色打印
},
)
dialector := sqlite.Open(":memory:?cache=shared")
if db, err = gorm.Open(dialector, &gorm.Config{
Logger: newLogger,
}); err != nil {
panic(err)
}
dba, err := db.DB()
dba.SetMaxOpenConns(1)
return db
}
func CloseMemoryDB(db *gorm.DB) {
sqlDB, _ := db.DB()
sqlDB.Close()
}
http mock
http.go文章來源地址http://www.zghlxwxcb.cn/news/detail-787909.html
package main
import (
"fmt"
"net/http"
"time"
)
func Send() (err error) {
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1:8080", nil)
if err != nil {
return
}
client := &http.Client{
Timeout: time.Second,
}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP status is %d", resp.StatusCode)
}
return
}
package main
import (
"net/http"
"testing"
"github.com/jarcoal/httpmock"
"github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
)
func TestSend(t *testing.T) {
convey.Convey("TestSend", t, func() {
convey.Convey("success", func() {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(http.MethodGet, "https://127.0.0.1:8080", httpmock.NewStringResponder(http.StatusOK, ""))
err := Send()
assert.Nil(t, err)
})
})
}
到了這里,關(guān)于GoLang 單元測試打樁和 mock的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!