背景
關(guān)于單元測試,其實(shí)是我們討論的非常多的一點(diǎn)粒没,作為一個測試人員筛婉,筆者唯一沒怎么接觸的測試,其實(shí)就是單元測試癞松。這段時間剛好在開發(fā)一些平臺爽撒,在代碼中也涉及到了這塊,因此記錄一下自己的一些想法响蓉。
筆者用一個場景來說明一下思路硕勿。
開發(fā)一個查詢接口,接受頁面?zhèn)魅氲膮?shù)枫甲,再查詢配置服務(wù)獲取數(shù)據(jù)庫的配置信息源武。最后拼成SQL之后查詢結(jié)果返回扼褪。
一個常見的代碼
筆者這里用Go寫一個偽代碼來演示,忽略那些特有的語法粱栖,相信單純看邏輯應(yīng)該是沒問題的迎捺。
func GetSomething(c *Request) {
userName := c.Query("user_name")
page := c.Query("page")
size := c.Query("size")
if userName == ""{
return error
}
if page == 0{
return error
}
if size == 0{
return error
}
rsp, err := QryDbInfoFromConfigCenter()
if err != nil{
return err
}
database := rsp.Get("Database")
table := rsp.Get("Table")
if database == ""{
return error
}
if table == ""{
return error
}
sql := fmt.Sprintf("select * from %s.%s where username='%s' and offset %d limit %d", database, table, userName, page, size)
resp, err := DoQryInfo(tableAddr, sql)
if err != nil{
return err
}
if resp.Code != 0{
return error
}
for(i:=0;i<len(resp.Data);i++){
if resp.Data[i].status == 0{
resp.Data[i].nickStatus = "成功"
} else if resp.Data[i].status == 1{
resp.Data[i].nickStatus = "失敗"
}
}
return resp.Data
}
上面的代碼是一個非常典型的寫法,這種線性的寫法幾乎存在于接觸的80%的代碼中查排。毫無疑問它是能夠正常工作的,并且書寫也非常方便抄沮,整個流程符合正常的線性思維跋核。
但是,如果要對這樣的代碼去做單元測試叛买,幾乎沒辦法進(jìn)行單元測試砂代。因?yàn)樗拿總€步驟都耦合在一起,如果要測試率挣,就必須準(zhǔn)備一個查詢db配置的服務(wù)刻伊,準(zhǔn)備一個有數(shù)據(jù)的db。這樣做單元測試的成本確實(shí)是非常高椒功,測試用例于環(huán)境強(qiáng)關(guān)聯(lián)捶箱,局限性非常大,并且跟做集成測試幾乎沒有區(qū)別动漾。
筆者眼中的單元測試
筆者眼中的單元測試應(yīng)該有這么幾點(diǎn):
- 不跟任何環(huán)境綁定丁屎,任意一個環(huán)境都能執(zhí)行
- 要能夠覆蓋代碼中所有于外部調(diào)用之外的代碼
- 外部依賴不使用Mock或者部署真實(shí)服務(wù)來處理,而是放棄旱眯,留給集成測試晨川。
改動原則
這里其實(shí)涉及到了代碼的變動,爭議應(yīng)該是非常大的删豺,筆者這里闡述自己的理解共虑。
這個查詢功能大致是這樣:接收請求數(shù)據(jù)->檢查數(shù)據(jù)是否合法->查詢DB信息->檢查返回信息的合法->對數(shù)據(jù)做一定的轉(zhuǎn)換(生成SQL)->請求DB查詢->解析返回結(jié)果->返回結(jié)果做一定的處理->返回。
大致可以分成這10步呀页,其中除去開頭的接受數(shù)據(jù)和返回結(jié)果妈拌,有8步。其中外部依賴的是2步赔桌,查詢db信息
和請求db查詢
供炎。其他的步驟都是一些數(shù)據(jù)的轉(zhuǎn)換和處理。那么代碼應(yīng)該把這些抽離出來作為單一功能的方法疾党,這樣單純的數(shù)據(jù)處理的方法音诫,就能夠不依賴任何環(huán)境從而進(jìn)行單元測試驗(yàn)證。
從這個思路來推導(dǎo)
- 請求數(shù)據(jù)合法性檢查沒問題雪位,就可以保證沒有非法的參數(shù)進(jìn)到流程中竭钝。
- 查詢配置中心返回的數(shù)據(jù)是合法的,就可以保證拼出來的SQL是正確的。
- 生成SQL的邏輯沒有問題香罐,就可以保證請求db查詢的數(shù)據(jù)沒有問題卧波。
- 查詢回來的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換沒有問題,那么返回的數(shù)據(jù)就不會有問題庇茫。
總結(jié)一下港粱,就是通過這樣的拆分,確保了我們請求外部服務(wù)的時候旦签,參數(shù)一定是按照約定傳的查坪,如果有改動破壞了這個約定,單元測試就能發(fā)現(xiàn)宁炫。同樣偿曙,返回數(shù)據(jù)的解析處理也是按照約定處理的,如果有改動破壞了這個約定羔巢,單元測試也是能夠發(fā)現(xiàn)的望忆。
如果有了這樣的保證,那么外部服務(wù)是否真的去請求竿秆,實(shí)際上區(qū)別并不是特別大启摄。
改動代碼的結(jié)構(gòu)
同樣用Go的偽代碼來寫這個改造后的代碼。
type Rqst struct{
UserName string
Page int32
Size int32
}
type DatabaseInfo struct {
Database string
Table string
}
func NewRqst(c *Request) *Rqst{
var r = new(Rqst)
r.UserName := c.Query("user_name")
r.Page := c.Query("page")
r.Size := c.Query("size")
}
func(r Rqst)checkParam()error{
if r.UserName == ""{
return error
}
if r.Page == 0{
return error
}
if r.Size == 0{
return error
}
}
func (r Rqst)buildQrySQL(d *DatabaseInfo)string{
sql := fmt.Sprintf("select * from %s.%s where username='%s' and offset %d limit %d", d.Database, d.Table, r.UserName, r.Page, r.Size)
}
func NewQryRsp(rsp *QryResp) *DatabaseInfo{
var d = new(DatabaseInfo)
d.Database := rsp.Get("Database")
d.Table := rsp.Get("Table")
return d
}
func(d *DatabaseInfo)checkResp(){
if d.Database == ""{
return error
}
if d.Table == ""{
return error
}
return d
}
func checkQryDbResult(resp *DoQryInfoResp)error{
if resp.Code != 0{
return error
}
return nil
}
func setNickStatus(resp *DoQryInfoResp){
for(i:=0;i<len(resp.Data);i++){
if resp.Data[i].status == 0{
resp.Data[i].nickStatus = "成功"
} else if resp.Data[i].status == 1{
resp.Data[i].nickStatus = "失敗"
}
}
}
func GetSomething(c *gin.Context) {
r := NewRqst(c)
err := r.checkParam()
if err != nil{
return err
}
_rsp, err := QryDbInfoFromConfigCenter()
if err != nil{
return err
}
rsp := NewQryRsp(_rsp)
err = rsp.checkResp()
if err != nil{
return err
}
sql := r.buildQrySQL(rsp)
resp, err := DoQryInfo(tableAddr, sql)
err = checkQryDbResult(resp)
if err != nil {
return err
}
setNickStatus(resp)
return resp.Data
}
從改造的結(jié)果來看袍辞,代碼變多了很多鞋仍,主要就是更多的結(jié)構(gòu)體的定義和方法聲明的代碼,實(shí)際的業(yè)務(wù)代碼來看是差不多的搅吁。
但是改造后的優(yōu)點(diǎn)確非常的明顯威创,主流程GetSomething
中的代碼更清晰簡單,閱讀的人可以很快的明白這個接口到底只做什么的谎懦,而不需要完全讀懂這個代碼肚豺。
同樣,改造后界拦,每個方法都是可以單獨(dú)的寫對應(yīng)的測試用例吸申,而這樣寫出來的單元測試用例由于只是一些數(shù)據(jù)變動的處理邏輯,沒有涉及到外部的請求享甸,因此是可以在任意環(huán)境執(zhí)行截碴,并且結(jié)果可靠有效,不會出現(xiàn)環(huán)境問題導(dǎo)致的用例失敗蛉威。
總結(jié)和一些思考
這樣的做法日丹,實(shí)際上涉及到了代碼結(jié)構(gòu)的變動,個人認(rèn)為這樣寫會更加優(yōu)雅易讀蚯嫌,但是由于每個人的想法哲虾、思維方式等都不同丙躏,包括工作經(jīng)歷,也會影響這些束凑,因此關(guān)于代碼優(yōu)雅性這塊不做更多的討論晒旅,讀者可以保留自己的想法。
對于單元測試來說汪诉,筆者做過一部分代碼改造來實(shí)踐這部分內(nèi)容废恋,發(fā)現(xiàn)效果還不錯,確確實(shí)實(shí)幫忙發(fā)現(xiàn)了一些問題扒寄,應(yīng)該說拴签,是具有一定的合理性的。
最后還有一點(diǎn)關(guān)于代碼覆蓋率的旗们,業(yè)界普遍的要求單元測試的覆蓋率是80%。按照筆者最近的實(shí)踐來看构灸,如果你的代碼沒有寫很多廢話或者廢邏輯上渴,這么干要達(dá)到80%,對于業(yè)務(wù)不復(fù)雜(也就是數(shù)據(jù)處理部分少)的工程來說還是有難度的喜颁。這或許是另一個值得探討和學(xué)習(xí)的點(diǎn)稠氮。