<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      單元測試結合web后臺項目實戰(zhàn)(重構和mock)

      背景介紹

      經過上一節(jié) 對單元測試框架的基本學習,我們已經掌握了 單元測試的基本寫法

      但是 對一個web 后臺項目來說,往往需要依賴一些基礎服務(數(shù)據(jù)庫、緩存等),實際生產環(huán)境當然是要連接這些基礎服務的,但是測試環(huán)境不一定能連通這些服務(測試環(huán)境最好是要能),因此編寫mock 也是一個比較必要的過程。

      如何讓測試代碼寫得更好,結合業(yè)務代碼更優(yōu)雅,也讓后人能夠更方便地開發(fā),是一個需要探索的過程。這篇文章將會通過我的一個測試項目的重構過程,介紹如何更好地從單測框架的選用,到項目的重構,再到最后單測代碼(數(shù)據(jù)庫mock)的編寫,一個完整的流程

      我自己的項目git 地址
      跟重構相關的提交

      測試框架選用

      sqlmock(不夠通用)

      數(shù)據(jù)庫操作這塊,有一個開源的mock 框架,sqlmock

      但是這限于 使用 原生sql.DB 類,使用起來才方便,如果業(yè)務用到了其他 orm 框架(如:gorm)就不方便了

      業(yè)務代碼抽象 + testify + gomonkey 結合

      最終選用了這種方式

      測試框架為什么選擇 testify + gomonkey 前一篇博客有介紹:使用更加方便,和業(yè)務代碼耦合性也不強

      業(yè)務代碼需要抽象成對象的原因:原來的業(yè)務方法都是直接放到業(yè)務的公共方法中,層級感不強
      重構的基本思路就是 將數(shù)據(jù)庫連接池管理、業(yè)務操作管理 都抽象成對象

      最終當所有資源層、service 層和 controller 層都封裝成對象的時候,就不會再有這三層中的對象,在var 中直接初始化業(yè)務對象的情況了(最多初始化鎖),這樣寫起mock 來才會更方便

      實際實現(xiàn)

      框架設計

      業(yè)務代碼重構具體實現(xiàn)

      首先項目架構還是比較經典的 controller - service - 數(shù)據(jù)層架構,下面從這三層分別介紹重構方式

      DB 層

      原實現(xiàn):所有的DB 操作方法,都放到mysql 這一個package 的公共方法中,也沒有封裝成對象的方法
      service 和 controller 層其實都是這么實現(xiàn)的,因此三層的重構思路都是類似的:將package 的公共方法 抽象到對象的方法中

      // https://gitee.com/atamagaii/mygoproject/blob/307b555ed5d6969169f27e05dc08aa3bf20ffdd2/src/database/mysql/schooldata.go
      package mysql
      ......
      func GetStudentSimpleInfoById(dbSchool *gorm.DB, studentId string) *model.Student {
      	currentStudent := new(model.Student)
      	err := dbSchool.Where("id = ?", studentId).Select("id, name, sex").Offset(0).Limit(10).
      		First(&currentStudent).Error
      	if nil != err {
      		log.Printf("[GetStudentSimpleInfoById] 查詢DB錯誤,請檢查: %s", err.Error())
      	}
      	return currentStudent
      }
      

      現(xiàn)實現(xiàn):按上圖的設計,首先會有一個DB 資源管理對象,另外具體業(yè)務的數(shù)據(jù),也會有一個獨立的管理類,如下:

      package resource
      ......
      var (
      	// 數(shù)據(jù)庫連接管理 單例
      	dbControllerInstance DBControllerInterface
      	// 數(shù)據(jù)庫連接管理單例 獲取鎖
      	dbControllerInstanceLock = sync.Once{}
      )
      
      // 獲取數(shù)據(jù)庫連接管理單例
      func NewDBController() DBControllerInterface {
      	dbControllerInstanceLock.Do(func() {
      		dbControllerInstance = new(GormController)
      		dbControllerInstance.InitResource()
      	})
      	return dbControllerInstance
      }
      
      // 數(shù)據(jù)庫連接管理 接口定義
      type DBControllerInterface interface {
      	InitResource() error
      	GetStudentController() studentsql.StudentDataInterface
      }
      
      // Gorm 連接資源管理 實際實現(xiàn)類
      type GormController struct {
      	// 學生數(shù)據(jù)處理controller
      	studentController studentsql.StudentDataInterface
      }
      
      // gorm 控制類: 獲取學校數(shù)據(jù)業(yè)務操作對象
      func (controller *GormController) GetStudentController() studentsql.StudentDataInterface {
      	return controller.studentController
      }
      
      // Gorm 連接初始化
      // 包含兩部分初始化:gorm 連接初始化、數(shù)據(jù)操作初始化(將初始化成功的gorm 連接池綁定過去)
      func (controller *GormController) InitResource() error {
      	dbSchool := config.GetDBConnection(mysql.DB_NAME_LOCAL)
      	controller.studentController = new(studentsql.StudentDataController)
      	controller.studentController.SetDBResource(dbSchool)
      	return nil
      }
      
      // Gorm 連接資源管理,mock 類
      type GormMockController struct {
      	studentController studentsql.StudentDataInterface
      }
      
      // gorm mock 類 資源初始化
      func (controller *GormMockController) InitResource() error {
      	controller.studentController = new(studentsql.StudentDataMockController)
      	controller.studentController.SetDBResource(nil)
      	return nil
      }
      ......
      

      DB 資源管理類:分別定義了 DBControllerInterface、GormController 和 GormMockController ,參考前面的架構圖示例,分別表示 DB 控制器接口定義、實際實現(xiàn)類 和 mock 類。

      package studentsql
      ......
      // 學生數(shù)據(jù)處理 接口
      type StudentDataInterface interface {
      	SetDBResource(*gorm.DB)
      	GetStudentSimpleInfoById(string) *model.Student
      	GetStudentSimpleInfoByName(string) *model.Student
      	UpdateStudentGrade(*model.Student) int64
      	GetGradeSummaryByGradeId(string) []*model.GradeSummary
      	GetAllStudentName() []string
      }
      
      // 學生信息處理 實際實現(xiàn)類
      type StudentDataController struct {
      	// gorm 連接池
      	dbSchool *gorm.DB
      }
      
      // 設置gorm 連接池,初始化的時候要執(zhí)行
      func (controller *StudentDataController) SetDBResource(db *gorm.DB) {
      	controller.dbSchool = db
      }
      
      // 通過學生id獲取學生的基本信息
      func (controller *StudentDataController) GetStudentSimpleInfoById(studentId string) *model.Student {
      	currentStudent := new(model.Student)
      	err := controller.dbSchool.Where("id = ?", studentId).Select("id, name, sex").Offset(0).Limit(10).
      		First(&currentStudent).Error
      	if nil != err {
      		log.Printf("[GetStudentSimpleInfoById] 查詢DB錯誤,請檢查: %s", err.Error())
      	}
      	return currentStudent
      }
      ......
      
      // 學生信息處理 mock 類
      type StudentDataMockController struct{}
      
      // mock: 設置gorm 連接池
      func (controller *StudentDataMockController) SetDBResource(db *gorm.DB) {
      	log.Printf("[mock] mock db init, will not actually init!")
      }
      ......
      

      學生數(shù)據(jù)處理類:同樣定義了三個類:StudentDataInterface、StudentDataController和 StudentDataMockController,關系和 DB 資源控制類 是類似的。
      InitResource 方法的實現(xiàn)原理:因為 DBController 是要給 上層(service層)用的,因此 DBController 和 StudentDataController 本身的對應關系要自己確認清楚,比如 GormMockController 初始化 其StudentDataController 對象的時候,就要用 StudentDataMockController ,也就是都要用mock 類。

      tips:單例模式鎖這里用到了 sync.Once 來實現(xiàn),參考博客

      service

      service 層主要是對SchoolService 對象的抽象,原實現(xiàn):

      package schoolservice
      
      ......
      
      func GetStudentById(id string) *model.Student {
      	log.Printf("[GetStudentById] 獲取學生信息,學生id: %s", id)
      	return mysql.GetStudentSimpleInfoById(mysql.DBSchool, id)
      }
      ......
      

      現(xiàn)實現(xiàn):

      package schoolservice
      
      ......
      
      // 學校業(yè)務處理類
      type SchoolService struct {
      	dbController dbresource.DBControllerInterface
      }
      
      var (
      	// 學校業(yè)務處理單例對象
      	schoolServiceInstance *SchoolService
      	// 獲取學校 業(yè)務處理單例對象鎖
      	schoolServiceInstanceLock = sync.Once{}
      )
      
      // 獲取學校業(yè)務處理對象
      func NewSchoolService() *SchoolService {
      	schoolServiceInstanceLock.Do(func() {
      		schoolServiceInstance = new(SchoolService)
      		schoolServiceInstance.dbController = dbresource.NewDBController()
      	})
      	return schoolServiceInstance
      }
      
      // 根據(jù)學生id 獲取學生信息
      func (service *SchoolService) GetStudentById(id string) *model.Student {
      	log.Printf("[GetStudentById] 獲取學生信息,學生id: %s", id)
      	return service.dbController.GetStudentController().GetStudentSimpleInfoById(id)
      }
      ......
      

      controller

      重構思路同上,現(xiàn)實現(xiàn):

      package schoolcontroller
      
      ......
      
      // 學校業(yè)務邏輯控制層
      type SchoolController struct {
      	schoolService *schoolservice.SchoolService
      }
      
      var (
      	// 學校controller 單例
      	schoolControllerInstance *SchoolController
      	// 學校controller 單例鎖
      	schoolControllerInstanceLock = sync.Once{}
      )
      
      // 獲取學校controller 對象 單例
      func NewSchoolController() *SchoolController {
      	schoolControllerInstanceLock.Do(func() {
      		schoolControllerInstance = new(SchoolController)
      		schoolControllerInstance.initSchoolController()
      	})
      	return schoolControllerInstance
      }
      
      // 初始化學校controller
      func (controller *SchoolController) initSchoolController() {
      	controller.schoolService = schoolservice.NewSchoolService()
      }
      
      // 通過學生id 獲取學生信息(只查DB)
      func (controller *SchoolController) GetStudentById(context *gin.Context) {
      	studentId := context.Query("id")
      	student := controller.schoolService.GetStudentById(studentId)
      	context.JSON(200, student)
      }
      ......
      

      測試代碼實現(xiàn)

      通過上面重構的過程,我們了解到 DB 層是有專門的mock 方法的,因此只要將 實際DB controller 的初始化方法,mock 成 mock類的初始化方法就可以了:

      func TestGetStudentById(t *testing.T) {
      	currentMock := gomonkey.ApplyFunc(dbresource.NewDBController, dbresource.NewDBMockController)
      	defer currentMock.Reset()
      	schoolService := schoolservice.NewSchoolService()
      	student := schoolService.GetStudentById("1")
      	assert.NotEqual(t, "", student.Name)
      }
      

      關鍵就是第一步:將 dbresource.NewDBController mock 成 dbresource.NewDBMockController
      后面照常執(zhí)行業(yè)務邏輯就行,由于DB controller 已經被mock 了,最終DB 層邏輯也會走mock 類的邏輯。至于你是想讓mock 類查測試DB,還是只是 返回一個虛擬的查詢結果,就看實際測試的需求了
      建議 修改操作 還是要連接測試DB ,真正跑數(shù)據(jù)庫方法執(zhí)行(修改操作影響比較大,最好有一個實際驗證的過程),查詢操作返回一個虛擬結果就行了

      總結

      • 從可測試性來看:只有把對象封裝好,對象的初始化都改成主動調用初始化方法,而不是使用 init 方法,或者直接在 var 中初始化,后續(xù)所有的方法、對象才能更方便地寫mock。
        如上面介紹,如果后續(xù)業(yè)務要新加一個接口,只需要在 業(yè)務實際實現(xiàn)類(StudentDataController) 和 mock 類(StudentDataMockController) 中定義新接口就可以了。
        同樣,如果開發(fā)想要給一個查詢接口,新加一種查詢場景,也是只要修改實際的業(yè)務方法(StudentDataController.GetStudentSimpleInfoById),和在mock 類的方法(StudentDataMockController.GetStudentSimpleInfoById) 中,新加對應場景的邏輯就可以了,結構非常清楚。

      • 從資源初始化來看:初始化操作都改成主動之后,只有第一次“被用到”的資源才會被初始化,服務剛啟動,還沒有流量進來的時候,是不會做任何初始化動作的,避免在服務依賴外部資源的時候,啟動需要花費非常長的時間。
        不過,這當然也要求了服務在資源初始化的時候,需要做更多的異常檢測和提醒機制。

      • 從服務運營能力來看:每一層都抽象成對象之后,基本就不存在什么執(zhí)行業(yè)務邏輯 還要用到全局變量的情況了,所有的資源都是通過一個“管理員對象” 來操作的。這樣封裝好之后,后續(xù)還可以做更多事情:比如DB層可以做 可用連接數(shù)的自監(jiān)控,service 層可以做常用方法統(tǒng)計,controller 層可以做針對用戶級別的流量控制,等等。

      通過這次對自己測試項目的重構,我更加意識到了:任何需求都是要經過詳細的設計,要思考很多可擴展性、可測試性,之后再實現(xiàn),才能夠真正對“需求”本身負責,寫出來的代碼和功能才能夠真正“可維護”。

      只是為了實現(xiàn)需求的目標而寫代碼,最終寫出來的代碼對自己、對后人都將會是災難性的后果。

      posted @ 2024-12-18 23:14  頭がいい天才  閱讀(44)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国产精品成人中文字幕| 国产精品福利自产拍久久| 激情五月天一区二区三区| 久久成人国产精品免费软件| 精品日韩亚洲av无码| 国产对白叫床清晰在线播放| 日韩精品一区二区三区蜜臀| 静安区| 亚洲www永久成人网站| 日韩丝袜欧美人妻制服| 成人午夜福利免费专区无码| 亚洲av成人网在线观看| 高清dvd碟片 生活片| 国产午夜精品理论大片| 女厕偷窥一区二区三区| 国产日韩综合av在线| 麻豆a级片| 337p粉嫩大胆噜噜噜| 国产精品揄拍一区二区久久| 久爱无码精品免费视频在线观看 | 亚洲第四色在线中文字幕| 久久人人爽人人爽人人av | 亚洲国产日韩欧美一区二区三区| 狠狠婷婷色五月中文字幕| 男女做aj视频免费的网站| 亚洲欧美人成人综合在线播放| 精品国产乱码久久久人妻| 隆德县| 欧美一本大道香蕉综合视频| 国产超高清麻豆精品传媒麻豆精品| 免费看欧美日韩一区二区三区| 日本三线免费视频观看| 五月综合激情婷婷六月| 国产91麻豆视频免费看| 师宗县| 麻豆国产成人AV在线播放| 人妻无码∧V一区二区| 影音先锋人妻啪啪av资源网站| 日韩精品一区二区三区激情视频| 国产三级无码内射在线看| 午夜福利国产区在线观看|