自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go項(xiàng)目實(shí)戰(zhàn)--數(shù)據(jù)Dao層代碼的單元測(cè)試實(shí)戰(zhàn)

數(shù)據(jù)庫(kù) MongoDB
采用sqlmock類(lèi)的工具,對(duì)Dao要執(zhí)行的SQL作出預(yù)期匹配,同時(shí)Mock SQL查詢(xún)要返回的數(shù)據(jù),保證Dao方法內(nèi)部的邏輯正常執(zhí)行。

Dao的單元測(cè)試

講到數(shù)據(jù)庫(kù)的單元測(cè)試,一般有那么幾個(gè)流派

  • 專(zhuān)門(mén)準(zhǔn)備一個(gè)獨(dú)立的數(shù)據(jù)庫(kù),單元測(cè)試時(shí)讓所有測(cè)試用例讀寫(xiě)這個(gè)獨(dú)立的數(shù)據(jù)庫(kù),它的優(yōu)點(diǎn)是單測(cè)真的去讀寫(xiě)數(shù)據(jù)庫(kù)啦,缺點(diǎn)嘛也顯而易見(jiàn),一個(gè)項(xiàng)目的數(shù)據(jù)庫(kù)不是光有表就行,還得準(zhǔn)備測(cè)試數(shù)據(jù),這個(gè)搞起來(lái)就有點(diǎn)麻煩,尤其是關(guān)聯(lián)性強(qiáng)的數(shù)據(jù),造起來(lái)更麻煩。
  • 讓項(xiàng)目在單元測(cè)試時(shí)訪問(wèn)內(nèi)存數(shù)據(jù)庫(kù),它的優(yōu)缺點(diǎn)其實(shí)跟上個(gè)差不多。
  • 采用sqlmock類(lèi)的工具,對(duì)Dao要執(zhí)行的SQL作出預(yù)期匹配,同時(shí)Mock SQL查詢(xún)要返回的數(shù)據(jù),保證Dao方法內(nèi)部的邏輯正常執(zhí)行。

我們這里采用的是第三個(gè)流派,用 sqlmock 方式來(lái)做數(shù)據(jù)庫(kù)Dao的單元測(cè)試,本節(jié)的內(nèi)容大綱主要如下:

圖片圖片

這里我們會(huì)用到DataDog家開(kāi)發(fā)的go-sqlmock這個(gè)工具,先來(lái)安裝一下它:

github.com/DATA-DOG/go-sqlmock

安裝過(guò)程如下:

圖片圖片

單元測(cè)試入口TestMain的設(shè)置

我們計(jì)劃在 UserDao 和 OrderDao 中找?guī)讉€(gè)典型的方法來(lái)做單元測(cè)試的實(shí)戰(zhàn),這里我們先在新建test/dao/user_test.go,創(chuàng)建完之后還不能馬上開(kāi)始寫(xiě)測(cè)試用例,我們?cè)賮?lái)做一下dao層單元測(cè)試的基礎(chǔ)工作。

在TestMain方法中初始化go-sqlmock ,這樣整個(gè)dao下的測(cè)試用例就都能使用它了,TestMain是在當(dāng)前package下最先運(yùn)行的一個(gè)函數(shù),無(wú)論你運(yùn)行哪個(gè)測(cè)試用例TestMain都會(huì)先被Go調(diào)用,所以它常用于測(cè)試基礎(chǔ)組件的初始化。

我們的TestMain的代碼如下:

var (
 mock sqlmock.Sqlmock
 err  error
 db   *sql.DB
)

func TestMain(m *testing.M) {
 db, mock, err = sqlmock.New()
if err != nil {
panic(err)
 }
// 把項(xiàng)目使用的DB連接換成sqlmock的DB連接
 dbMasterConn, _ := gorm.Open(mysql.New(mysql.Config{
  Conn:                      db,
  SkipInitializeWithVersion: true,
  DefaultStringSize:         0,
 }))
 dbSlaveConn, _ := gorm.Open(mysql.New(mysql.Config{
  Conn:                      db,
  SkipInitializeWithVersion: true,
  DefaultStringSize:         0,
 }))
 dao2.SetDBMasterConn(dbMasterConn)
 dao2.SetDBSlaveConn(dbSlaveConn)
 os.Exit(m.Run())
}

這里我們創(chuàng)建一個(gè) go-sqlmock 的數(shù)據(jù)庫(kù)連接 和 mock對(duì)象,mock對(duì)象管理 db 預(yù)期要執(zhí)行的SQL,具體初始化中各個(gè)參數(shù)的作用,直接看我上面代碼里的注視吧。

因我我們項(xiàng)目里Dao使用的數(shù)據(jù)庫(kù)連接在包外不可訪問(wèn),所以我在這里給項(xiàng)目dao層里加了 SetDBMasterConn,SetDBSlaveConn兩個(gè)方法把我們?cè)镜臄?shù)據(jù)庫(kù)連接替換成了sqlmock的數(shù)據(jù)庫(kù)連接。

基礎(chǔ)設(shè)置完成后,接下來(lái)我們分別找Dao的Insert、Update、Select操作來(lái)展示怎么給他們做單元測(cè)試。

Insert 操作的單元測(cè)試

首先給UserDao的CreateUser方法做單元測(cè)試,它是用戶(hù)注冊(cè)接口的邏輯中會(huì)用到的Dao方法,其定義如下:

func (ud *UserDao) CreateUser(userInfo *do.UserBaseInfo, userPasswordHash string) (*model.User, error) {
 userModel := new(model.User)
 err := util.CopyProperties(userModel, userInfo)
if err != nil {
  err = errcode.Wrap("UserDaoCreateUserError", err)
returnnil, err
 }
 userModel.Password = userPasswordHash

 err = DBMaster().WithContext(ud.ctx).Create(userModel).Error
if err != nil {
  err = errcode.Wrap("UserDaoCreateUserError", err)
returnnil, err
 }
return userModel, nil
}

這里就不再對(duì)CreateUser這個(gè)方法里都是什么做展開(kāi)了,大家直接看項(xiàng)目代碼吧,它的單元測(cè)試如下:

func TestUserDao_CreateUser(t *testing.T) {
    userInfo := &do.UserBaseInfo{
        Nickname:  "Slang",
        LoginName: "slang@go-mall.com",
        Verified:  0,
        Avatar:    "",
        Slogan:    "happy!",
        IsBlocked: 0,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }
    passwordHash, _ := util.BcryptPassword("123456")
    userIsDel := 0

    ud := dao2.NewUserDao(context.TODO())
    mock.ExpectBegin()
    mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `users`")).
    WithArgs(userInfo.Nickname, userInfo.LoginName, passwordHash, userInfo.Verified, userInfo.Avatar,
               userInfo.Slogan, userIsDel, userInfo.IsBlocked, userInfo.CreatedAt, userInfo.UpdatedAt).
    WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit()
    userObj, err := ud.CreateUser(userInfo, passwordHash)
    assert.Nil(t, err)
    assert.Equal(t, userInfo.LoginName, userObj.LoginName)
}

這里我們首先自己初始化了一個(gè)CreateUser會(huì)用到的數(shù)據(jù)userInfo和passwordHash,然后使用 ExpectExec 指定預(yù)期要執(zhí)行的SQL以及預(yù)期返回的結(jié)果。

這里我來(lái)說(shuō)明一下sqlmock 默認(rèn)使用 sqlmock.QueryMatcherRegex 作為默認(rèn)的SQL匹配器,該匹配器使用mock.ExpectQuery 和 mock.ExpectExec 的參數(shù)作為正則表達(dá)式與真正執(zhí)行的SQL語(yǔ)句進(jìn)行匹配,如果使用QueryMatcherEqual 作為匹配器的話,那么我們寫(xiě)預(yù)期SQL時(shí)就要寫(xiě)完整的SQL了。

我推薦用默認(rèn)的匹配器就行,因?yàn)榻酉聛?lái)的WithArgs中我們還要給SQL的 ? 占位符提供參數(shù)值,這個(gè)參數(shù)值如果數(shù)量或者類(lèi)型匹配不上的話,單測(cè)依然是無(wú)法通過(guò)的。

WillReturnResult(sqlmock.NewResult(1, 1)) 這行的意思是SQL執(zhí)行后返回的 lastInsertId 是 1, 受影響行數(shù)也是 1。

拿到結(jié)果之后我們?cè)僮鯽ssert斷言,判斷結(jié)果是否符合預(yù)期。符合預(yù)期則通過(guò),不符合的話測(cè)試用例會(huì)失敗。大家可以自己嘗試修改一下這個(gè)用例看它執(zhí)行失敗的效果。

Select 查詢(xún)的單元測(cè)試

關(guān)于SQL查詢(xún)的單元測(cè)試,和上面的區(qū)別是我們會(huì)Mock返回的結(jié)果集,這里我們拿的是OrderDao的GetUserOrders做的單元測(cè)試,代碼如下。

func TestOrderDao_GetUserOrders(t *testing.T) {
    orderDel := soft_delete.DeletedAt(0)
    now := time.Now()
    emptyPayTime := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)

    orders := []*model.Order{
        {1, "12345675555", "", 1, 1, 100, 100, 0, 0, emptyPayTime, orderDel, now, now},
        {2, "12345675556", "", 1, 1, 100, 100, 0, 0, emptyPayTime, orderDel, now, now},
    }
    od := dao2.NewOrderDao(context.TODO())
    var userId int64 = 1
    offset := 10
    limit := 50
    mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `orders`")).WithArgs(userId, orderDel, limit, offset).
    WillReturnRows(
        sqlmock.NewRows([]string{"id", "order_no", "pay_trans_id", "pay_type", "user_id", "bill_money", "pay_money",
                                  "pay_state", "order_status", "paid_at", "is_del", "created_at", "updated_at"}).
        AddRow(
            orders[0].ID, orders[0].OrderNo, orders[0].PayTransId, orders[0].PayType, orders[0].UserId, orders[0].BillMoney, orders[0].PayMoney,
            orders[0].PayState, orders[0].OrderStatus, orders[0].PaidAt, orders[0].IsDel, orders[0].CreatedAt, orders[0].UpdatedAt,
        ).AddRow(
            orders[1].ID, orders[1].OrderNo, orders[1].PayTransId, orders[1].PayType, orders[1].UserId, orders[1].BillMoney, orders[1].PayMoney,
            orders[1].PayState, orders[1].OrderStatus, orders[1].PaidAt, orders[1].IsDel, orders[1].CreatedAt, orders[1].UpdatedAt,
        ),
    )
    mock.ExpectQuery(regexp.QuoteMeta("SELECT count(*) FROM `orders`")).WithArgs(userId, orderDel).
    WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(2))
    gotOrders, totalRow, err := od.GetUserOrders(userId, offset, limit)
    assert.Nil(t, err)
    assert.Equal(t, orders, gotOrders)
    assert.Equal(t, totalRow, int64(2))
}

這里我用 ExpectQuery 指定了兩個(gè)預(yù)期要執(zhí)行的SQL是為什么呢?因?yàn)镚etUserOrders方法即返回了用戶(hù)訂單列表還返回了數(shù)據(jù)分頁(yè)用的totalRaws變量,大家可以試試把它刪掉看看這個(gè)單元測(cè)試能不能執(zhí)行成功,這里我可以告訴你結(jié)果會(huì)成功但又沒(méi)完全成功,會(huì)有一條Warning警告,報(bào)告出有一個(gè)執(zhí)行的SQL沒(méi)有做預(yù)期匹配。

執(zhí)行單元測(cè)試時(shí)可以用上面我教的命令,也可以用IDE自帶的測(cè)試按鈕跑來(lái)跑這個(gè)測(cè)試用例。

Update操作的單元測(cè)試

Update操作的單元測(cè)試于Insert操作的類(lèi)似,我們選用OrderDao的UpdateOrderStatus 方法來(lái)做單元測(cè)試。

func TestOrderDao_UpdateOrderStatus(t *testing.T) {
 orderNewStatus := 1
 var orderId int64 = 1
 orderDel := 0
 mock.ExpectBegin()
 mock.ExpectExec(regexp.QuoteMeta("UPDATE `orders` SET")).
  WithArgs(orderNewStatus, AnyTime{}, orderId, orderDel).
  WillReturnResult(sqlmock.NewResult(1, 1))
 mock.ExpectCommit()
 od := dao2.NewOrderDao(context.TODO())
 err := od.UpdateOrderStatus(orderId, orderNewStatus)
 assert.Nil(t, err)
}

這里的AnyTime是咱們自定義的一個(gè)類(lèi)型

type AnyTime struct{}

func (a AnyTime) Match(v driver.Value) bool {
 // Match 方法中:判斷字段值只要是time.Time 類(lèi)型,就能驗(yàn)證通過(guò)
 _, ok := v.(time.Time)
 return ok
}

其實(shí)在使用SQL完全匹配模式時(shí)才必須用它,因?yàn)閰?shù)提供的Time.Now()做為UpdatedAt的時(shí)間,這與SQL執(zhí)行時(shí)真正的UpdateAt時(shí)間是有很小的差異的,這個(gè)時(shí)候我們可以提供AnyTime做為更新時(shí)間,這樣sqlmock在做預(yù)期SQL和實(shí)際SQL的匹配時(shí),遇到了AnyTime類(lèi)型的預(yù)期值,就會(huì)按照這里指定的規(guī)則,判斷字段值只要是time.Time 類(lèi)型就能驗(yàn)證通過(guò)。

總結(jié)

本節(jié)代碼版本為c19.1

git fetch --tags
git checkout tags/c19.1

訪問(wèn) https://github.com/go-study-lab/go-mall/compare/c18...c19.1 可在線查看詳細(xì)的代碼更新。

責(zé)任編輯:武曉燕 來(lái)源: 網(wǎng)管叨bi叨
相關(guān)推薦

2022-04-08 09:01:56

腳本Go應(yīng)用單元

2021-04-23 07:33:10

SpringSecurity單元

2017-01-14 23:42:49

單元測(cè)試框架軟件測(cè)試

2022-08-02 08:07:24

單元測(cè)試代碼重構(gòu)

2024-01-09 08:08:12

Go單元測(cè)試系統(tǒng)

2023-07-27 08:16:51

數(shù)據(jù)訪問(wèn)層項(xiàng)目

2023-10-28 10:10:41

2022-10-26 08:00:49

單元測(cè)試React

2023-07-26 08:58:45

Golang單元測(cè)試

2017-03-30 07:56:30

測(cè)試前端代碼

2011-05-16 16:52:09

單元測(cè)試徹底測(cè)試

2009-06-26 17:48:38

JSF項(xiàng)目單元測(cè)試JSFUnit

2011-07-27 17:02:12

Xcode iPhone 單元測(cè)試

2021-09-18 15:40:03

Vue單元測(cè)試命令

2017-01-16 12:12:29

單元測(cè)試JUnit

2017-01-14 23:26:17

單元測(cè)試JUnit測(cè)試

2023-07-28 10:27:48

Java單元測(cè)試

2011-06-14 15:56:42

單元測(cè)試

2020-08-18 08:10:02

單元測(cè)試Java

2022-05-12 09:37:03

測(cè)試JUnit開(kāi)發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)