偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Go項(xiàng)目實(shí)戰(zhàn)-學(xué)會(huì)對(duì)代碼邏輯層進(jìn)行BDD測(cè)試

開(kāi)發(fā) 項(xiàng)目管理
通過(guò)這幾節(jié)單元測(cè)試實(shí)戰(zhàn)的內(nèi)容大家應(yīng)該能體會(huì)到,我們?yōu)轫?xiàng)目做好分層設(shè)計(jì)的一個(gè)優(yōu)點(diǎn)--好測(cè)試。每個(gè)分層都有具體的職責(zé),每塊代碼的邊界不至于過(guò)大,這樣我們做單元測(cè)試代碼寫(xiě)起來(lái)會(huì)更簡(jiǎn)單。

前面兩節(jié)我們的單元測(cè)試主要集中在對(duì)項(xiàng)目基礎(chǔ)設(shè)施層的代碼進(jìn)行單元測(cè)試,針對(duì)Dao數(shù)據(jù)操作層我們講解了如何在不實(shí)際對(duì)項(xiàng)目數(shù)據(jù)庫(kù)進(jìn)行CURD的情況下使用了sqlmock的方式進(jìn)行單元測(cè)試。而對(duì)于外部API對(duì)接層則是教會(huì)大家用gock實(shí)現(xiàn)無(wú)侵入的HTTP Mock,對(duì)有API請(qǐng)求的代碼進(jìn)行單元測(cè)試。

今天我們更進(jìn)一步,從項(xiàng)目代碼的基礎(chǔ)設(shè)施層來(lái)到邏輯層和用戶(hù)接口層。邏輯層的代碼肯定更注重邏輯,所以我們?cè)谶@里會(huì)引入goconvey 這個(gè)庫(kù)實(shí)現(xiàn),讓它幫助我們實(shí)現(xiàn)BDD(行為驅(qū)動(dòng)測(cè)試),goconvey支持樹(shù)形結(jié)構(gòu)方便構(gòu)造各種場(chǎng)景,讓我們能更容易地基于 goconvey 來(lái)組織的單測(cè)。本文大綱如下:

圖片圖片

goconvey 的 安裝命令如下:

go get github.com/smartystreets/goconvey

輸入命令后,安裝過(guò)程如下所示:

圖片圖片

關(guān)于goconvey的使用方法詳解,這里就不在給大家舉簡(jiǎn)單的例子進(jìn)行說(shuō)明了,還是按照前面幾篇的風(fēng)格,給大家提供一個(gè)我在公眾號(hào)上寫(xiě)的 goconvey 入門(mén)詳解。

  • 使用 Go Convey 做BDD測(cè)試的入門(mén)指南

邏輯層單元測(cè)試實(shí)戰(zhàn)

我們項(xiàng)目各業(yè)務(wù)的核心邏輯都主要集中在領(lǐng)域服務(wù) domainservice 中,按照我們?yōu)轫?xiàng)目做的的單元測(cè)試目錄規(guī)劃,它的單元測(cè)試_test.go 文件都應(yīng)該放在test/domainservice 目錄中。

.
|---test
|     |---controller    # controller 的測(cè)試用例
|     |---dao    # dao 的測(cè)試用例
|     |---domainservice # 邏輯層領(lǐng)域服務(wù)的測(cè)試用例
|     |---library # 外部API對(duì)接的測(cè)試用例

TestMain 入口設(shè)置

依照慣例,在每個(gè)要寫(xiě)單元測(cè)試的package中,我門(mén)都需要在包內(nèi)測(cè)試的統(tǒng)一入口TestMain中做一些公共基礎(chǔ)性的工作。

我們?cè)赥estMain中加上Convey 的SuppressConsoleStatistics和PrintConsoleStatistics,用于在測(cè)試完成后輸出測(cè)試結(jié)果。

package domainservice

import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)

func TestMain(m *testing.M) {
// convey在TestMain場(chǎng)景下的入口
 SuppressConsoleStatistics()
 result := m.Run()
// convey在TestMain場(chǎng)景下的結(jié)果打印
 PrintConsoleStatistics()
 os.Exit(result)
}

這么設(shè)置后,輸出的測(cè)試結(jié)果會(huì)按照單測(cè)中Convey書(shū)寫(xiě)的層級(jí)分層級(jí)顯示,這個(gè)輸出結(jié)果我會(huì)在下面的實(shí)戰(zhàn)案例中展示給大家。

注意這里convey包的導(dǎo)入方式使用了 import . 的語(yǔ)法,import . "github.com/smartystreets/goconvey/convey",這樣是為了方便大家直接使用 convey 包中的各種定義,無(wú)需再像 convey.Convey 這樣加包前綴。

實(shí)戰(zhàn)案例一:密碼復(fù)雜度的BDD測(cè)試

在案例一種我們找一個(gè)相對(duì)簡(jiǎn)單的工具函數(shù)來(lái)演示怎么用convey幫助我們組織用例。我們?cè)谟脩?hù)注冊(cè)和重設(shè)密碼種使用過(guò)一個(gè)檢查用戶(hù)密碼復(fù)雜度的工具函數(shù)。

func PasswordComplexityVerify(s string) bool {
var (
  hasMinLen  = false
  hasUpper   = false
  hasLower   = false
  hasNumber  = false
  hasSpecial = false
 )
iflen(s) >= 8 {
  hasMinLen = true
 }
for _, char := range s {
switch {
case unicode.IsUpper(char):
   hasUpper = true
case unicode.IsLower(char):
   hasLower = true
case unicode.IsNumber(char):
   hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
   hasSpecial = true
  }
 }
return hasMinLen && hasUpper && hasLower && hasNumber && hasSpecial
}

接下來(lái)我們就給 PasswordComplexityVerify 函數(shù)編寫(xiě)測(cè)試用例。

func TestPasswordComplexityVerify(t *testing.T) {
 Convey("Given a simple password", t, func() {
  password := "123456"
  Convey("When run it for password complexity checking", func() {
   result := util.PasswordComplexityVerify(password)
   Convey("Then the checking result should be false", func() {
    So(result, ShouldBeFalse)
   })
  })
 })

 Convey("Given a complex password", t, func() {
  password := "123@1~356Wrx"
  Convey("When run it for password complexity checking", func() {
   result := util.PasswordComplexityVerify(password)
   Convey("Then the checking result should be true", func() {
    So(result, ShouldBeTrue)
   })
  })
 })
}

在這個(gè)測(cè)試函數(shù)中,首先我們從正向和負(fù)向兩個(gè)方面對(duì)函數(shù)進(jìn)行單元測(cè)試,正向測(cè)試和負(fù)向測(cè)試都是什么呢,用通俗易懂的文字解釋就是:

  • 正向測(cè)試:提供正確的入?yún)?,期待被測(cè)對(duì)象返回正確的結(jié)果。
  • 負(fù)向測(cè)試:提供錯(cuò)誤的入慘,期待被測(cè)對(duì)象返回錯(cuò)誤的結(jié)果或者對(duì)應(yīng)的異常。

通過(guò)這個(gè)例子,正好說(shuō)一下在使用goconvy的過(guò)程中需要注意的幾個(gè)點(diǎn):

  • Convey 可以嵌套的,這樣我們就可以構(gòu)造出來(lái)一條測(cè)試的場(chǎng)景路徑,幫助我們寫(xiě)出BDD風(fēng)格的單測(cè)。
  • Convey 嵌套使用時(shí)函數(shù)的參數(shù)有區(qū)別。

最上層Convey 為Convey(description string, t *testing.T, action func())

其他層級(jí)的嵌套 Convey 不需要傳入 *testing.T,為Convey(description string, action func())

結(jié)合我們?cè)?description 參數(shù)中的描述,我們就可以建立起來(lái)類(lèi)似 BDD (行為驅(qū)動(dòng)測(cè)試)的語(yǔ)義:

  • Given【給定某些初始條件】

Given a simple passowrd 給定一個(gè)簡(jiǎn)單密碼

  • When 【當(dāng)一些動(dòng)作發(fā)生后】
  • When run it for password complexity checking 當(dāng)對(duì)它進(jìn)行復(fù)雜度檢查時(shí)
  • Then 【結(jié)果應(yīng)該是】
  • Then the checking result should be false 結(jié)果應(yīng)該是 false

BDD測(cè)試中的描述信息通常使用的是Given、When、Then引導(dǎo)的狀語(yǔ)從句,如果喜歡用中文寫(xiě)描述信息也要記得使用類(lèi)似語(yǔ)境的句子。

咱們用 go test -v 命令來(lái)看看測(cè)試運(yùn)行的效果,我們可以看到輸出的測(cè)試結(jié)果會(huì)按照單測(cè)中Convey書(shū)寫(xiě)的層級(jí),分層級(jí)顯示。

圖片圖片

實(shí)戰(zhàn)案例二:用戶(hù)注冊(cè)的BDD測(cè)試

通過(guò)上面一個(gè)相對(duì)簡(jiǎn)單的例子,相信大家對(duì)goconvey庫(kù)的使用已經(jīng)有所了解,那么接下來(lái)我們?cè)賮?lái)看一下,怎么為邏輯層中那些復(fù)雜的代碼邏輯編寫(xiě)單元測(cè)試。

我選用的是用戶(hù)注冊(cè)的領(lǐng)域服務(wù)方法,來(lái)給大家展示為業(yè)務(wù)邏輯代碼編寫(xiě)單元測(cè)試,整個(gè)測(cè)試用 goconvey 組織用例的行為路徑,使用 gomonkey 對(duì) RegisterUser 方法中依賴(lài)的其他方法進(jìn)行Mock,整個(gè)測(cè)試方法的代碼如下:

func TestUserDomainSvc_RegisterUser(t *testing.T) {
 Convey("Given a user for RegisterUser of UserDomainSvc", t, func() {
  givenUser := &do.UserBaseInfo{
   Nickname:  "Kevin",
   LoginName: "kevin@go-mall.com",
   Verified:  0,
   Avatar:    "",
   Slogan:    "Keep tang ping",
   IsBlocked: 0,
   CreatedAt: time.Date(2025, 1, 31, 23, 28, 0, 0, time.Local),
   UpdatedAt: time.Date(2025, 1, 31, 23, 28, 0, 0, time.Local),
  }
  planPassword := "123@1~356Wrx"
var s *dao.UserDao
// 讓UserDao的CreateUser返回Mock數(shù)據(jù)
  gomonkey.ApplyMethod(s, "CreateUser", func(_ *dao.UserDao, user *do.UserBaseInfo, password string) (*model.User, error) {
   passwordHash, _ := util.BcryptPassword(planPassword)
   userResult := &model.User{
    ID:        1,
    Nickname:  givenUser.Nickname,
    LoginName: givenUser.LoginName,
    Verified:  givenUser.Verified,
    Password:  passwordHash,
    Avatar:    givenUser.Avatar,
    Slogan:    givenUser.Slogan,
    CreatedAt: givenUser.CreatedAt,
    UpdatedAt: givenUser.UpdatedAt,
   }
   return userResult, nil
  })

  Convey("When the login name of user is not occupied", func() {
   gomonkey.ApplyMethod(s, "FindUserByLoginNam", func(_ *dao.UserDao, loginName string) (*model.User, error) {
    returnnew(model.User), nil
   })
   Convey("Then user should be created successfully", func() {
    user, err := domainservice.NewUserDomainSvc(context.TODO()).RegisterUser(givenUser, planPassword)
    So(err, ShouldBeNil)
    So(user.ID, ShouldEqual, 1)
    So(user, ShouldEqual, givenUser)
   })
  })

  Convey("When the login name of user has already been occupied by other users", func() {
   gomonkey.ApplyMethod(s, "FindUserByLoginNam", func(_ *dao.UserDao, loginName string) (*model.User, error) {
    return &model.User{LoginName: givenUser.LoginName}, nil
   })
   Convey("Then the user's registration should be unsuccessful", func() {
    user, err := domainservice.NewUserDomainSvc(context.TODO()).RegisterUser(givenUser, planPassword)
    So(user, ShouldBeNil)
    So(err, ShouldNotBeNil)
    So(err, ShouldEqual, errcode.ErrUserNameOccupied)
   })
  })
 })
}

在這個(gè)測(cè)試方法中,我在頂層Convey中嵌套了兩個(gè)并列的Convey方法來(lái)組織正向和負(fù)向的單元測(cè)試,之所以不跟上面那個(gè)案例一樣寫(xiě)兩個(gè)并列的頂層Convey方法是因?yàn)楸粶y(cè)方法 RegisterUser 的入?yún)?shù)太難構(gòu)造,這也正好給大家展示了我們使用Convey設(shè)計(jì)單元測(cè)試的行為路徑時(shí)的靈活性。

這里我們提供了兩個(gè)測(cè)試用例,正向用例中讓 RegisterUser 依賴(lài)的Dao方法 CreateUser 返回創(chuàng)建成功的結(jié)果,預(yù)期 RegisterUser 返回正確的結(jié)果。

而負(fù)向用例中則讓 CreateUser 返回用戶(hù)名在數(shù)據(jù)庫(kù)中已存在時(shí)返回的結(jié)果,同時(shí)預(yù)期 RegisterUser 會(huì)返回用戶(hù)名已被占用的錯(cuò)誤 errcode.ErrUserNameOccupied 。

最后咱們用 go test -v命令來(lái)看看測(cè)試運(yùn)行的效果:

圖片圖片

Controller 的單元測(cè)試

到現(xiàn)在為止我們的單元測(cè)試實(shí)戰(zhàn)案例已經(jīng)覆蓋了數(shù)據(jù)訪問(wèn)Dao層、API對(duì)接層和領(lǐng)域服務(wù)層。還剩下一個(gè)用戶(hù)接口層沒(méi)有涉及到,即項(xiàng)目的Controller方法該怎么做單元測(cè)試呢?

首先我覺(jué)得,按照我們項(xiàng)目的分層架構(gòu)來(lái)說(shuō)Controller是負(fù)責(zé)接受和驗(yàn)證請(qǐng)求和調(diào)用下層拿到結(jié)果返回響應(yīng)的,在這里包含核心業(yè)務(wù)邏輯。如果我們能把它依賴(lài)的下層的單元測(cè)試做到位,Controller的單元測(cè)試可以不做。

不過(guò)我們知道有個(gè)驗(yàn)證項(xiàng)目質(zhì)量的數(shù)據(jù)指標(biāo)叫:測(cè)試覆蓋率,這個(gè)指標(biāo)肯定越高越好,所以這里我在簡(jiǎn)單地把Controller 處理函數(shù)的單元測(cè)試給大家過(guò)一下。

在 Web 項(xiàng)目中 Controller 里都是API接口的請(qǐng)求處理函數(shù),為它們編寫(xiě)單元測(cè)試需要用到Go自帶的net/http/httptest包, 它可以mock一個(gè)HTTP請(qǐng)求和響應(yīng)記錄器,讓我們的 server 端接收并處理我們 mock 的HTTP請(qǐng)求,同時(shí)使用響應(yīng)記錄器來(lái)記錄 server 端返回的響應(yīng)內(nèi)容。

這里我們那用戶(hù)登陸這個(gè)接口給大家演示它的Controller函數(shù)是怎么做單元測(cè)試的,它的單元測(cè)試如下。

func TestLoginUser(t *testing.T) {
 Convey("Given right login name and password", t, func() {
  loginName := "yourName@go-mall.com"
  password := "12Qa@6783Wxf3~!45"

  Convey("When use them to Login through API /user/login", func() {
   var s *appservice.UserAppSvc
   gomonkey.ApplyMethod(s, "UserLogin", func(_ *appservice.UserAppSvc, _ *request.UserLogin) (*reply.TokenReply, error) {
    LoginReply := &reply.TokenReply{
     AccessToken:   "70624d19b6644b0bbf8169f51fb5a91f132edebc",
     RefreshToken:  "d16e22fef5cb7f6c69355c9a3c6ce8d1d3b37a84",
     Duration:      7200,
     SrvCreateTime: "2025-02-01 15:34:35",
    }
    return LoginReply, nil
   })

   var b bytes.Buffer
   json.NewEncoder(&b).Encode(map[string]string{"login_name": loginName, "password": password})
   req := httptest.NewRequest(http.MethodPost, "/user/login", &b)
   req.Header.Set("platform", "H5")
   gin.SetMode(gin.ReleaseMode) // 不讓它在控制臺(tái)里輸出路由信息
   g := gin.New()
   router.RegisterRoutes(g)
   // mock一個(gè)響應(yīng)記錄器
   w := httptest.NewRecorder()
   // 讓server端處理mock請(qǐng)求并記錄返回的響應(yīng)內(nèi)容
   g.ServeHTTP(w, req)

   Convey("Then the user will login successfully", func() {
    So(w.Code, ShouldEqual, http.StatusOK)
    // 檢驗(yàn)響應(yīng)內(nèi)容是否復(fù)合預(yù)期
    var resp map[string]interface{}
    json.Unmarshal([]byte(w.Body.String()), &resp)
    respData := resp["data"].(map[string]interface{})
    So(respData["access_token"], ShouldNotBeEmpty)
   })
  })
 })

在這個(gè)單元測(cè)試中我們還是會(huì)用 goconvey來(lái)組織測(cè)試的行為路徑,用 gomonkey 給Controller函數(shù)調(diào)用的應(yīng)用服務(wù)方法做打樁返回Mock結(jié)果,不然就跟用POSTMAN 請(qǐng)求接口一樣咧,那樣的話(huà)如果下層代碼里有數(shù)據(jù)庫(kù)CURD更新之類(lèi)操作的話(huà)還是會(huì)去實(shí)際訪問(wèn)數(shù)據(jù)庫(kù)的,這顯然不是我們想要的。

對(duì)于Controller方法的驗(yàn)證主要聚焦于請(qǐng)求參數(shù)的驗(yàn)證以及響應(yīng)結(jié)果的驗(yàn)證,因?yàn)?Controller 在我們項(xiàng)目的分層設(shè)計(jì)中就只干這兩件事。

總結(jié)

通過(guò)這幾節(jié)單元測(cè)試實(shí)戰(zhàn)的內(nèi)容大家應(yīng)該能體會(huì)到,我們?yōu)轫?xiàng)目做好分層設(shè)計(jì)的一個(gè)優(yōu)點(diǎn)--好測(cè)試。每個(gè)分層都有具體的職責(zé),每塊代碼的邊界不至于過(guò)大,這樣我們做單元測(cè)試代碼寫(xiě)起來(lái)會(huì)更簡(jiǎn)單。如果把所有邏輯都耦合在Controller 函數(shù)那種代碼,寫(xiě)單元測(cè)試的難度先不說(shuō),有效性也很難保證,因?yàn)闇y(cè)試的顆粒度太大必然導(dǎo)致很難測(cè)出代碼內(nèi)部的問(wèn)題。

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

2025-04-28 01:55:00

工具sqlmockSQL

2024-12-05 09:13:55

Go項(xiàng)目模塊

2021-07-28 08:53:53

GoGDB調(diào)試

2025-05-07 09:06:03

2024-02-21 19:02:05

Go模板化方式

2023-04-26 00:41:36

A/B測(cè)試郵件數(shù)量

2022-09-27 09:21:34

SOLID開(kāi)閉原則Go

2022-01-17 07:50:37

Go代碼規(guī)范

2023-05-26 00:06:05

2015-06-15 11:45:55

2021-11-29 22:59:34

Go Dockertest集成

2025-03-20 07:01:40

2025-03-28 07:50:00

端到端測(cè)試Go語(yǔ)言

2020-09-27 14:00:44

代碼

2024-02-21 19:56:48

??filterA并發(fā)計(jì)算

2022-11-09 07:18:18

驅(qū)動(dòng)測(cè)試BDD

2021-07-02 17:22:50

前端TDDBDD

2022-07-19 08:01:55

函數(shù)Go格式化

2022-08-30 09:57:28

項(xiàng)目傳統(tǒng)對(duì)象訪問(wèn)

2019-12-24 09:31:55

機(jī)器人人工智能編程
點(diǎn)贊
收藏

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