【云圖說】第132期 小云妹帶您快速玩轉RDS實例操作(2)——刪除與退訂
2873
2025-04-01
1.通用Mock方式
類似Java Mockito。testify和mockery結合使用,testify是一個golang測試框架,主要有assert、mock和test suite三個特性,mockery利用testify的mock來生成mock的代碼。
testify包下載: go get github.com/stretchr/testify mockery安裝: go get github.com/vektra/mockery/.../
mockery會根據定義的interface生成對應的mock struct。
示例代碼
common/etcd/client.go
common/etcd/mocks/EtcdClient.go
sql-driver/rds/config/loader/remote_configuration_loader_test.go
1. 生成mock strcut
命令行執行go generate 或者使用goland直接生成,此處會自動創建mocks目錄,以及對應的mock struct文件。
生成的代碼如下所示:
// Code generated by mockery v1.0.0. DO NOT EDIT. package mocks import ( etcd "github.com/huaweicloud/devcloud-go/common/etcd" mock "github.com/stretchr/testify/mock" clientv3 "go.etcd.io/etcd/client/v3" ) // EtcdClient is an autogenerated mock type for the EtcdClient type type EtcdClient struct { mock.Mock } // Close provides a mock function with given fields: func (_m *EtcdClient) Close() error { ret := _m.Called() var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // Del provides a mock function with given fields: key func (_m *EtcdClient) Del(key string) (int64, error) { ret := _m.Called(key) var r0 int64 if rf, ok := ret.Get(0).(func(string) int64); ok { r0 = rf(key) } else { r0 = ret.Get(0).(int64) } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(key) } else { r1 = ret.Error(1) } return r0, r1 } // Get provides a mock function with given fields: key func (_m *EtcdClient) Get(key string) (string, error) { ret := _m.Called(key) var r0 string if rf, ok := ret.Get(0).(func(string) string); ok { r0 = rf(key) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(key) } else { r1 = ret.Error(1) } return r0, r1 } // List provides a mock function with given fields: prefix func (_m *EtcdClient) List(prefix string) ([]*etcd.KeyValue, error) { ret := _m.Called(prefix) var r0 []*etcd.KeyValue if rf, ok := ret.Get(0).(func(string) []*etcd.KeyValue); ok { r0 = rf(prefix) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*etcd.KeyValue) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(prefix) } else { r1 = ret.Error(1) } return r0, r1 } // Put provides a mock function with given fields: key, value func (_m *EtcdClient) Put(key string, value string) (string, error) { ret := _m.Called(key, value) var r0 string if rf, ok := ret.Get(0).(func(string, string) string); ok { r0 = rf(key, value) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(key, value) } else { r1 = ret.Error(1) } return r0, r1 } // Watch provides a mock function with given fields: prefix, startIndex, onEvent func (_m *EtcdClient) Watch(prefix string, startIndex int64, onEvent func(*clientv3.Event)) { _m.Called(prefix, startIndex, onEvent) }
2. 業務代碼調用EtcdClient
此處定義了一個RemoteConfigurationLoader,其成員etcdClient的類型為上述定義的EtcdClient interface,在Get方法中調用EtcdClient的Get方法。
type RemoteConfigurationLoader struct { etcdClient etcd.EtcdClient dataSourceKey string routerKey string activeKey string listeners []config.RouterConfigurationListener } func (l *RemoteConfigurationLoader) Get()(string, string, string) { dataSourceConfig, err := l.etcdClient.Get(l.dataSourceKey) if err != nil { return "", "", "" } routerConfig, err := l.etcdClient.Get(l.routerKey) if err != nil { log.Printf("ERROR: get remote routerConfig failed, %v", err) return "", "", "" } active, err := l.etcdClient.Get(l.activeKey) if err != nil { log.Printf("ERROR: get remote active failed, %v", err) return "", "", "" } return dataSourceConfig, routerConfig, active }
3. 測試代碼編寫
編寫對RemoteConfigurationLoader的Get()方法的測試代碼,對EtcdClient的Get方法進行mock。
1 import ( 2 "fmt" 3 "testing" 4 5 "github.com/huaweicloud/devcloud-go/common/etcd/mocks" 6 "github.com/stretchr/testify/assert" 7 ) 8 9 func TestRemoteConfigurationLoader_Get(t *testing.T) { 10 mockClient := &mocks.EtcdClient{} 11 loader := &RemoteConfigurationLoader{ 12 dataSourceKey: "datasourceKey", 13 routerKey: "routerKey", 14 activeKey: "activeKey", 15 etcdClient: mockClient, 16 } 17 mockClient.On("Get", loader.dataSourceKey).Return("data", nil).Once() 18 mockClient.On("Get", loader.routerKey).Return("router", nil).Once() 19 mockClient.On("Get", loader.activeKey).Return("active", nil).Once() 20 datasource, router, active := loader.Get() 21 assert.Equal(t, "data", datasource) 22 assert.Equal(t, "router", router) 23 assert.Equal(t, "active", active) 24 }
其中17-19行代碼就是在mock我們想要的數據。mockClient調用On方法,首先傳入要mock的方法名字,然后傳入方法參數,此處是利用golang的反射來實現的。Return方法中傳入想要mock的返回數據,最后調用Once()方法表示此方法只執行一次。
4. 參考文檔
https://segmentfault.com/a/1190000016897506
https://www.xuanzhangjiong.top/2019/10/12/mockery%E4%BB%8B%E7%BB%8D%E5%8F%8A%E4%BD%BF%E7%94%A8/
https://github.com/vektra/mockery
2. Mysql Mock
2.1 mock mySQL Server(推薦)
利用 github.com/dolthub/go-mysql-server,go-mysql-server基于Mysql語法,解析標準sql,它可以在內存中啟動一個mySQL Server。
安裝:
go get github.com/dolthub/go-mysql-server
示例代碼:
package main import ( "fmt" "time" sqle "github.com/dolthub/go-mysql-server" "github.com/dolthub/go-mysql-server/auth" "github.com/dolthub/go-mysql-server/memory" "github.com/dolthub/go-mysql-server/server" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/information_schema" ) const ( user = "user" passwd = "pass" address = "localhost" port = "13306" dbName = "test" tableName = "pets" ) func main() { engine := sqle.NewDefault( sql.NewDatabaseProvider( createTestDatabase(), information_schema.NewInformationSchemaDatabase(), )) config := server.Config{ Protocol: "tcp", Address: fmt.Sprintf("%s:%s", address, port), Auth: auth.NewNativeSingle(user, passwd, auth.AllPermissions), } s, err := server.NewDefaultServer(config, engine) if err != nil { panic(err) } go func() { s.Start() }() fmt.Println("mysql-server started!") <- make(chan interface{}) } func createTestDatabase() *memory.Database { db := memory.NewDatabase(dbName) table := memory.NewTable(tableName, sql.Schema{ {Name: "name", Type: sql.Text, Nullable: false, Source: tableName}, {Name: "email", Type: sql.Text, Nullable: false, Source: tableName}, {Name: "phone_numbers", Type: sql.JSON, Nullable: false, Source: tableName}, {Name: "created_at", Type: sql.Timestamp, Nullable: false, Source: tableName}, }) db.AddTable(tableName, table) ctx := sql.NewEmptyContext() rows := []sql.Row{ sql.NewRow("John Doe", "jasonkay@doe.com", []string{"555-555-555"}, time.Now()), sql.NewRow("John Doe", "johnalt@doe.com", []string{}, time.Now()), sql.NewRow("Jane Doe", "jane@doe.com", []string{}, time.Now()), sql.NewRow("Evil Bob", "jasonkay@gmail.com", []string{"555-666-555", "666-666-666"}, time.Now()), } for _, row := range rows { _ = table.Insert(ctx, row) } return db
2.2 mock sql driver
使用 DATA-DOG/go-sqlmock,該包實現了go sdk sql/driver的接口,本質上是一個mock驅動
安裝:
go get github.com/DATA-DOG/go-sqlmock
示例代碼
原生sql代碼使用 示例代碼見:https://github.com/DATA-DOG/go-sqlmock
編寫ut時利用定義的globalMock做dml前置操作,具體使用方法見官方文檔。
var ( globalOrm orm.Ormer once sync.Once mockOnce sync.Once globalMockOrm orm.Ormer GlobalMock sqlmock.Sqlmock ) func GetOrmer() orm.Ormer { if utils.GetenvOrDefault("isTest", "") == "true" { mockOnce.Do(func() { var db *sql.DB db, GlobalMock, _ = sqlmock.New() GlobalMock.ExpectPrepare("SELECT TIMEDIFF") GlobalMock.ExpectPrepare("SELECT ENGINE") globalMockOrm, _ = orm.NewOrmWithDB("mysql", "default", db) }) return globalMockOrm } once.Do(func() { // override the default value(1000) to return all records when setting no limit orm.DefaultRowsLimit = -1 globalOrm = orm.NewOrm() }) return globalOrm }
測試代碼
type Book struct { Id int64 `gorm:"column:id"` Title string `gorm:"column:title"` } func TestSqlMockBeegoOrm(t *testing.T) { os.Setenv("isTest", "true") ormer := driver_test.GetOrmer() GlobalMock.ExpectQuery("SELECT").WillReturnRows( sqlmock.NewRows([]string{"id", "title"}). AddRow(1, "one")) book := &Book{Id:1} err := ormer.Read(book) assert.Nil(t, err) assert.Equal(t, "one", book.Title) }
var ( globalGormDB *gorm.DB globalMockGormDB *gorm.DB globalMock sqlmock.Sqlmock once sync.Once mockOnce sync.Once ) func GetGormDB() *gorm.DB { if utils.GetenvOrDefault("isTest", "") == "true" { mockOnce.Do(func() { var db *sql.DB db, globalMock, _ = sqlmock.New() globalMockGormDB, _ = gorm.Open(mysql.New(mysql.Config{ Conn: db, SkipInitializeWithVersion: true, }), &gorm.Config{}) }) return globalMockGormDB } once.Do(func() { globalGormDB, _ = gorm.Open(mysql.Open("user:pass@tcp(127.0.0.1:3306)/dbname"), &gorm.Config{}) }) return globalGormDB }
測試代碼
func TestSqlMockGorm(t *testing.T) { os.Setenv("isTest", "true") gormDB := driver_test.GetGormDB() driver_test.GlobalMock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `books`")).WillReturnRows( sqlmock.NewRows([]string{"id", "title"}). AddRow(1, "one"). AddRow(2, "two")) var books []Book err := gormDB.Find(&books).Error assert.Nil(t, err) assert.Equal(t, 2, len(books)) assert.Equal(t, int64(1), books[0].Id) assert.Equal(t, "one", books[0].Title) assert.Equal(t, int64(2), books[1].Id) assert.Equal(t, "two", books[1].Title) }
2.3 使用優劣
個人感覺mock mysql server最方便,對代碼侵入較少,測試代碼也會更少,測試范圍會更廣。
mock sql driver只適合對業務層mock數據庫操作,測試業務代碼;當使用復雜sql時,需要將orm的鏈式操作等轉為一個復雜sql語句用于mock,需要編寫大量測試代碼。
而mock mysql server在此基礎上還能測試數據庫操作的代碼是否正確。
2.4 參考文檔
使用純Go實現的Mysql數據庫
https://pkg.go.dev/github.com/dolthub/go-mysql-server#section-readme
https://zhuanlan.zhihu.com/p/249313716
https://github.com/DATA-DOG/go-sqlmock
https://blog.csdn.net/weixin_44294408/article/details/120698482
3. Redis Mock
使用github.com/alicebob/miniredis/v2 ,miniredis可以在內存中啟動一個redis server,支持大部分redis命令,具體支持情況見github readme
示例代碼:
redis/devspore_client_test.go
import ( "context" "testing" "github.com/alicebob/miniredis/v2" "github.com/go-redis/redis/v8" "github.com/stretchr/testify/assert" ) func TestMockRedis(t *testing.T) { server, _ := miniredis.Run() client := redis.NewClient(&redis.Options{Addr: server.Addr()}) ctx := context.Background() client.Set(ctx, "test", "val", 0) res := client.Get(ctx, "test") assert.Nil(t, res.Err()) assert.Equal(t, "val", res.Val()) server.Close() }
4. Ginkgo測試框架
Ginkgo是一個BDD(Behavior Driven Development)風格的go測試框架,與Gomega配合使用,在需要寫大量單測時,特別是需要一些通用代碼時,Ginkgo可以使用BeforeEach和AfterEach將每個用例的通用步驟提取出來,會讓代碼看起來很清爽。
具體使用方法見 https://ke-chain.github.io/ginkgodoc/
示例代碼
devcloud-go/sql-driver/mysql/devspore_driver_test.go 使用Ginkgo框架編寫driver的CRUD測試代碼,結合mock mysql server使用。
import ( "database/sql" "fmt" "testing" "github.com/huaweicloud/devcloud-go/sql-driver/rds/config" "github.com/huaweicloud/devcloud-go/sql-driver/rds/datasource" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" sqle "github.com/dolthub/go-mysql-server" "github.com/dolthub/go-mysql-server/auth" "github.com/dolthub/go-mysql-server/memory" "github.com/dolthub/go-mysql-server/server" mocksql "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/information_schema" ) func TestGinkgoSuite(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "mysql") } var _ = Describe("CRUD", func() { var ( devsporeDB *sql.DB masterDB *sql.DB err error activeNode *datasource.NodeDataSource ) go startMockServer() BeforeEach(func() { devsporeDB, err = sql.Open("devspore_mysql", "../rds/resources/driver_test_config.yaml") Expect(err).NotTo(HaveOccurred()) activeNode, err = initDB() Expect(err).NotTo(HaveOccurred()) masterDB, err = sql.Open("mysql", activeNode.MasterDataSource.Dsn) Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { Expect(devsporeDB.Close()).NotTo(HaveOccurred()) Expect(masterDB.Close()).NotTo(HaveOccurred()) }) It("Test Query", func() { var ( val string flag bool ) err = devsporeDB.QueryRow("SELECT val FROM foo WHERE id=?", id1).Scan(&val) Expect(err).NotTo(HaveOccurred()) for _, slave := range activeNode.SlavesDatasource { if slave.Name == val { flag = true } } Expect(flag).To(Equal(true)) }) It("Test Insert", func() { var val string _, err = devsporeDB.Exec(`INSERT INTO foo (id, val) VALUES (?, ?)`, id2, "insert") Expect(err).NotTo(HaveOccurred()) err = masterDB.QueryRow("SELECT val FROM foo WHERE id=?", id2).Scan(&val) Expect(err).NotTo(HaveOccurred()) Expect(val).To(Equal("insert")) }) It("Test Update", func() { var val string _, err = devsporeDB.Exec(`UPDATE foo set val=? where id=?`, "update", id1) Expect(err).NotTo(HaveOccurred()) err = masterDB.QueryRow("SELECT val FROM foo WHERE id=?", id1).Scan(&val) Expect(err).NotTo(HaveOccurred()) Expect(val).To(Equal("update")) }) It("Test Delete", func() { var val string _, err = devsporeDB.Exec(`DELETE FROM foo where id=?`, id1) Expect(err).NotTo(HaveOccurred()) err = masterDB.QueryRow("SELECT val FROM foo WHERE id=?", id1).Scan(&val) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("sql: no rows in result set")) }) }) const ( user = "root" passwd = "root" address = "localhost" port = "13306" ) func startMockServer() { engine := sqle.NewDefault( mocksql.NewDatabaseProvider( memory.NewDatabase("ds0"), memory.NewDatabase("ds1"), memory.NewDatabase("ds0-slave0"), memory.NewDatabase("ds0-slave1"), memory.NewDatabase("ds1-slave0"), memory.NewDatabase("ds1-slave1"), information_schema.NewInformationSchemaDatabase(), )) config := server.Config{ Protocol: "tcp", Address: fmt.Sprintf("%s:%s", address, port), Auth: auth.NewNativeSingle(user, passwd, auth.AllPermissions), } s, err := server.NewDefaultServer(config, engine) if err != nil { panic(err) } go func() { s.Start() }() fmt.Println("mysql-server started!") }
5. 代碼參考
示例代碼標有文件路徑的均來自devcloud-go項目,具體可看https://github.com/huaweicloud/devcloud-go。本文是在本人開發devcloud-go過程中積累而成,各位看官可以移步devcloud-go項目點個star~
Go MySQL Redis 單元測試
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。