聊聊單元測試

背景

關(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):

  1. 不跟任何環(huán)境綁定丁屎,任意一個環(huán)境都能執(zhí)行
  2. 要能夠覆蓋代碼中所有于外部調(diào)用之外的代碼
  3. 外部依賴不使用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)

  1. 請求數(shù)據(jù)合法性檢查沒問題雪位,就可以保證沒有非法的參數(shù)進(jìn)到流程中竭钝。
  2. 查詢配置中心返回的數(shù)據(jù)是合法的,就可以保證拼出來的SQL是正確的。
  3. 生成SQL的邏輯沒有問題香罐,就可以保證請求db查詢的數(shù)據(jù)沒有問題卧波。
  4. 查詢回來的數(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)稠氮。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市半开,隨后出現(xiàn)的幾起案子隔披,更是在濱河造成了極大的恐慌,老刑警劉巖寂拆,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奢米,死亡現(xiàn)場離奇詭異,居然都是意外死亡纠永,警方通過查閱死者的電腦和手機(jī)鬓长,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來尝江,“玉大人涉波,你說我怎么就攤上這事√啃颍” “怎么了啤覆?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惭聂。 經(jīng)常有香客問我窗声,道長,這世上最難降的妖魔是什么彼妻? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任嫌佑,我火速辦了婚禮豆茫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘屋摇。我一直安慰自己揩魂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布炮温。 她就那樣靜靜地躺著火脉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪柒啤。 梳的紋絲不亂的頭發(fā)上倦挂,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機(jī)與錄音担巩,去河邊找鬼方援。 笑死,一個胖子當(dāng)著我的面吹牛涛癌,可吹牛的內(nèi)容都是我干的犯戏。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼拳话,長吁一口氣:“原來是場噩夢啊……” “哼先匪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起弃衍,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤呀非,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后镜盯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岸裙,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年速缆,在試婚紗的時候發(fā)現(xiàn)自己被綠了哥桥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡激涤,死狀恐怖拟糕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情倦踢,我是刑警寧澤送滞,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站辱挥,受9級特大地震影響犁嗅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晤碘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一褂微、第九天 我趴在偏房一處隱蔽的房頂上張望功蜓。 院中可真熱鬧,春花似錦宠蚂、人聲如沸式撼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽著隆。三九已至,卻和暖如春呀癣,著一層夾襖步出監(jiān)牢的瞬間美浦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工项栏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浦辨,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓沼沈,卻偏偏與公主長得像荤牍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子庆冕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 我是一個著迷于產(chǎn)品和運(yùn)營的技術(shù)人,樂于跨界的終身學(xué)習(xí)者劈榨。歡迎關(guān)注我的個人公眾號「跨界架構(gòu)師」每周五11:45 按時...
    跨界架構(gòu)師閱讀 243評論 0 1
  • 遇到問題多思考访递、多查閱、多驗(yàn)證同辣,方能有所得拷姿,再勤快點(diǎn)樂于分享,才能寫出好文章旱函。 一响巢、單元測試 1. 定義與特點(diǎn) 單...
    程序熊大閱讀 8,219評論 7 62
  • 作為一名質(zhì)量管理人員,從剛?cè)胄袝r就接觸到單元測試:需求提測時要保證一定的單元測試覆蓋率作為提測準(zhǔn)入棒妨;進(jìn)行線上問題c...
    Rechel_uniq閱讀 707評論 0 1
  • 本篇主要是聊一聊以下幾個方面的內(nèi)容: 為什么要單元測試 單元測試框架 單元測試的好處 單元測試與重構(gòu) 1. 為什么...
    塞外的風(fēng)閱讀 241評論 0 3
  • 一踪古、教程目標(biāo) 學(xué)會基于AssertJ的斷言技術(shù); 學(xué)會基于AssertJ-DB的數(shù)據(jù)庫斷言技術(shù)券腔; 學(xué)會基于JMoc...
    文景大大閱讀 1,364評論 1 0