努力寫(xiě)“好”代碼

代碼規(guī)范與質(zhì)量

本文是關(guān)于代碼規(guī)范和代碼質(zhì)量相關(guān)的主題牡昆。

隨著我們寫(xiě)的代碼越來(lái)越多砰诵,技術(shù)債務(wù)也就隨之升高。什么是技術(shù)債務(wù)呢菩咨?通俗來(lái)講就是我們?cè)谲浖_(kāi)發(fā)的過(guò)程中吠式,為了達(dá)到當(dāng)前的目標(biāo),寫(xiě)一些只對(duì)當(dāng)前工作有效的代碼旦委。在項(xiàng)目緊急的時(shí)候奇徒,這么做無(wú)可厚非,只要我們?cè)谑潞竽軌蚣皶r(shí)的償還這些“技術(shù)債務(wù)”就行了缨硝。在事后能夠及時(shí)償還“技術(shù)債務(wù)”,這是理想情況罢低,現(xiàn)實(shí)情況更多的是在代碼上寫(xiě)上“TODO:這個(gè)我們有時(shí)間再優(yōu)化”查辩,而眾所周知,TODO is NOT TODO网持∫说海“出來(lái)混遲早要還的”,“技術(shù)債”也是這樣功舀。

隨著項(xiàng)目一個(gè)接著一個(gè)萍倡,似乎每個(gè)項(xiàng)目又都是緊急的,“技術(shù)債”越積越高辟汰,當(dāng)初承諾要優(yōu)化的似乎永遠(yuǎn)沒(méi)有時(shí)間去優(yōu)化列敲。而我們?cè)陧?xiàng)目緊急的時(shí)候?qū)懙哪切坝植皇遣荒苡谩钡拇a,好像也變得很難理解了帖汞。想再在上面寫(xiě)這些勉強(qiáng)可用的代碼也變得困難了戴而,結(jié)果造成的情況就是“前方產(chǎn)品故障頻發(fā),后方開(kāi)發(fā)人員不停地?fù)浠稹薄?/p>

影響產(chǎn)品質(zhì)量的因素有很多翩蘸,這里只討論代碼質(zhì)量與產(chǎn)品質(zhì)量的關(guān)系所意,應(yīng)該怎樣去提高代碼質(zhì)量。本文將從以下三個(gè)方面來(lái)討論:

  • 代碼風(fēng)格與規(guī)范
  • 代碼重構(gòu)的方式
  • 單元測(cè)試

代碼風(fēng)格與規(guī)范

首先是為人編寫(xiě)程序,其次才是計(jì)算機(jī)扶踊。

代碼風(fēng)格無(wú)非就是那些不寫(xiě)也沒(méi)關(guān)系泄鹏,不會(huì)影響正常功能的東西,譬如命名秧耗、注釋命满、函數(shù)名等削锰。但是風(fēng)格良好的代碼墙懂,可讀性就更好偿枕,“挨揍”和“挨罵”的可能性就更少年局,而這些“揍”你和“罵”你的也許就是半年后的自己哑子。

可讀性基本定理

代碼的寫(xiě)法應(yīng)當(dāng)使別人(可能是6個(gè)月后的自己)理解它所需的時(shí)間最小化漱牵。要經(jīng)常地想一想其他人是不是會(huì)覺(jué)得你的代碼容易理解桐罕,這需要額外的時(shí)間宁否。這樣做就需要你打開(kāi)大腦中從前在編碼時(shí)可能沒(méi)有打開(kāi)的那部分功能缩麸。

命名

一個(gè)好的铸磅、有意義的命名能夠傳遞足夠多的信息,達(dá)到替代冗余注釋的效果杭朱,只通過(guò)名字就可以獲得大量的信息阅仔。

  • 名字要有意義

    在為函數(shù)或者變量去名字的時(shí)候,想一想這個(gè)名字需不需要注釋來(lái)補(bǔ)充說(shuō)明弧械,如果需要的話(huà)八酒,說(shuō)明取的名字不夠好。

    var d int32 // 文件創(chuàng)建時(shí)間刃唐,天
    var daysSinceCreation int32
    

    以上代碼中羞迷,第一行中的變量d就沒(méi)有第二行的daysSinceCreation更有意義。

  • 避免空泛的名字

    比如tmp,ret,foo,bar等無(wú)實(shí)際信息的名字画饥。當(dāng)然衔瓮,在作用域小,只是臨時(shí)存儲(chǔ)一下中間變量除外抖甘。

    tmp := user.Name()
    tmp += " " + user.Tel()
    tmp += " " + user.Score()
    

    以上代碼热鞍,userInfo就比tmp變量可讀性更好。

  • 使用更專(zhuān)業(yè)的名詞衔彻,更有表現(xiàn)力

    func GetPage() 
    func DownloadPage() 
    

    比如有個(gè)函數(shù)薇宠,作用是從網(wǎng)絡(luò)上獲取頁(yè)面信息,用GetPage容易造成疑惑米奸,不清楚是從數(shù)據(jù)庫(kù)還是從網(wǎng)絡(luò)上獲取數(shù)據(jù)昼接,使用DownloadPage就更專(zhuān)業(yè),更能體現(xiàn)這個(gè)函數(shù)的功能悴晰。

  • 用具體的名字代替抽象的名字

    在給變量慢睡、函數(shù)或者其他元素命名時(shí)逐工,要把它描述得更具體而不是更抽象。

    func ServerCanStart() 
    func CanListenOnPort()    // 更好
    

    以上代碼中CanListenOnPort比ServerCanStart`更具體漂辐。

    代碼中硬編碼的數(shù)字也更抽象泪喊,用一個(gè)便于搜索的變量替代。

  • 名字可以帶上其他重要信息

    比如髓涯,在值為毫秒的變量后面加上_ms袒啼,或者在還需要轉(zhuǎn)義的,未處理的變量前面加上raw_纬纪。

  • 類(lèi)名和方法名

    • 類(lèi)名和對(duì)象名使用名詞或名詞短語(yǔ)蚓再,如Customer、AddressParser()
    • 方法名使用動(dòng)詞或動(dòng)詞短語(yǔ)包各,如 DeleteUser() 摘仅、Save()

名字容易且重要,當(dāng)你看到一個(gè)毫無(wú)意義的變量或方法名時(shí)问畅,毫不猶豫的改掉娃属,現(xiàn)在的IDE都支持一鍵改名,幾乎毫不費(fèi)力的改善了代碼質(zhì)量护姆。

注釋

首先引用《Clean Code》中的幾句對(duì)注釋的描述:

“什么也比不上放置良好的注釋來(lái)得有用矾端。什么也不會(huì)比亂七八糟的注釋更有本事搞亂一個(gè)模塊。什么也不會(huì)比陳舊卵皂、提供錯(cuò)誤信息的注釋更具有破壞力”

“注釋是為了彌補(bǔ)代碼的不足”

“能不能不寫(xiě)注釋?zhuān)瑥拇a上就可以很清晰的了解用途”

好的代碼自己本身就是最好的文檔秩铆。當(dāng)你打算加注釋的時(shí)候,問(wèn)問(wèn)自己‘我如何才能把我的代碼改善到不需增加注釋?zhuān)俊貥?gòu)自己的代碼渐裂,然后使文檔讓其更清楚豺旬。 — Steve McConnell《代碼大全》的作者

從上面大概可以看出,該書(shū)的作者對(duì)注釋持負(fù)面態(tài)度柒凉,作者認(rèn)為好代碼 > 壞代碼 + 好注釋。

很多時(shí)候我們又不得不寫(xiě)一些注釋?zhuān)瑑H僅用代碼不能很清晰的描述我們的真實(shí)意圖篓跛。那么什么是好的注釋?zhuān)渴裁礃拥淖⑨屖菈淖⑨屇叵ダ蹋坑衷撊绾稳?xiě)呢?

注釋?xiě)?yīng)該是對(duì)“意圖”進(jìn)行詮釋?zhuān)玫淖⑨屟院?jiǎn)意賅愧沟,用最少的文字傳達(dá)了必要的信息蔬咬,例如以下幾種情況的注釋?zhuān)?/p>

  • 這些看起來(lái)很怪異的代碼為什么這么寫(xiě)
  • 代碼中埋下的“坑”是什么,“這個(gè)函數(shù)只有我才能調(diào)用通沐寺,且不能改”
  • 常量定義林艘,常量的用途,為何取這個(gè)值
  • 總結(jié)性的文字描述混坞,告訴別人這段代碼的工作機(jī)制
  • 產(chǎn)品/老板要求一定要有這個(gè)業(yè)務(wù)狐援,特殊處理

而壞注釋對(duì)代碼并沒(méi)有多少用處钢坦,甚至?xí)斐衫斫馍系恼系K,如下面這幾種情況:

  • 從代碼本身就能快速推斷的事實(shí)寫(xiě)注釋

    // Account 結(jié)構(gòu)體定義
    type Account struct{
    }
    // 設(shè)置賬戶(hù)名
    func (a *Account)SetName(name string){
    }
    
  • 給不好的名字加注釋

    // Release the handle for this key. This doesn't modify the actual registry.
    func DeleteRegistry(key string)
    
    func ReleaseRegistryHandle(key string)
    

    與其花額外的精力為不好的名字進(jìn)行注釋?zhuān)€不如取一個(gè)更好的名字啥酱。

  • 大量的廢棄代碼注釋

    現(xiàn)在都有版本管理爹凹,代碼不會(huì)丟失,在需要的時(shí)候看看文件提交歷史就能夠找回镶殷。這些代碼果斷刪除吧禾酱,讓代碼更干凈一點(diǎn)。

那我們注釋的時(shí)候又該如何寫(xiě)呢绘趋,下面有些小Tips可以作為參考颤陶。

  • 精簡(jiǎn)注釋?zhuān)屪⑨屆枋龊?jiǎn)潔

    // 學(xué)生的分?jǐn)?shù)情況,第一個(gè)map中的key是學(xué)生姓名陷遮,第二個(gè)map分別是課程名和對(duì)應(yīng)的分?jǐn)?shù)
    ScoreMap map[string]map[string]int32    
    
    // userName -> [project:score]
    ScoreMap map[string]map[string]int32
    

    上面兩個(gè)注釋都能說(shuō)明意圖滓走,但是第二個(gè)注釋顯得更簡(jiǎn)潔,不啰嗦拷呆。

  • 精確描述函數(shù)的行為

    // 返回文件的行數(shù)
    func CountLines(string fileName)
    // 統(tǒng)計(jì)文件中的 '\n' 換行符個(gè)數(shù)
    func CountLines(string fileName)
    

    第一個(gè)注釋描述的不夠清晰闲坎,例如空文件算0行還是1行?hello\nworld 算幾行茬斧?而第二個(gè)注釋就沒(méi)有歧義腰懂。

  • 用適當(dāng)例子說(shuō)明特殊情況

    // 移除 src 中的 chars 
    func Strip(src, chars string)string
    
    // Example:Strip("abba/a/ba", "ab") -> "/a/"
    func Strip(src, chars string)string
    

    第二個(gè)在注釋中添加恰當(dāng)?shù)睦樱f(shuō)明了Strip函數(shù)的意圖项秉。

  • 直接申明代碼意圖

    //add by xx  產(chǎn)品要求一定要給某些高級(jí)用戶(hù)增加xx功能绣溜。 時(shí)間:2020-04-30
    func Increase(userId string)
    

當(dāng)你感覺(jué)需要撰寫(xiě)注釋時(shí),請(qǐng)先嘗試重構(gòu)娄蔼,試著讓所有注釋都變得多余怖喻。

函數(shù)

函數(shù)應(yīng)該盡量寫(xiě)的短小,但是該寫(xiě)多小并沒(méi)有明確的標(biāo)準(zhǔn)岁诉,一般來(lái)說(shuō)20-30行最佳锚沸。Go語(yǔ)言中,一個(gè)函數(shù)如果很大涕癣,我們可以采用匿名函數(shù)將邏輯寫(xiě)的更清晰哗蜈。

一個(gè)函數(shù)只做一件事,不要把不相關(guān)的邏輯都堆在一個(gè)函數(shù)中坠韩,把這個(gè)函數(shù)變成了萬(wàn)能函數(shù)距潘。如果發(fā)現(xiàn)足夠的行數(shù)都是在解決不相關(guān)的字問(wèn)題時(shí),果斷的把它抽取到獨(dú)立的函數(shù)中只搁。

在寫(xiě)代碼時(shí)音比,可以問(wèn)問(wèn)自己下面這些問(wèn)題,有助于識(shí)別不相關(guān)代碼:

  • 看到某個(gè)函數(shù)或者代碼塊時(shí)氢惋,問(wèn)問(wèn)自己:這段代碼的高層次的目標(biāo)時(shí)什么洞翩?
  • 對(duì)于每一行代碼稽犁,問(wèn)一下:它是直接為了目標(biāo)而工作嗎?這段代碼高層次的目標(biāo)是什么呢菱农?

過(guò)度設(shè)計(jì)

程序員傾向于高估有多少功能真的對(duì)于他們的項(xiàng)目來(lái)講是必不可少的缭付。很多功能結(jié)果沒(méi)有完成,或者沒(méi)有用到循未,也可能只是讓程序更復(fù)雜陷猫。程序員還傾向于低估實(shí)現(xiàn)一個(gè)功能所要花的工夫。

有些用不到的功能并沒(méi)有想象中那么重要的妖,不一定會(huì)在未來(lái)用到绣檬,我們不需要為這些可能用不到的功能投入寶貴的時(shí)間。如一個(gè)內(nèi)部的系統(tǒng)嫂粟,本身沒(méi)幾個(gè)人用娇未,去要求系統(tǒng)的高可用、分布式星虹、微服務(wù)和國(guó)際化等明顯不合理的需求零抬。

邏輯

邏輯是代碼的血肉,清晰的邏輯會(huì)讓代碼更容易理解宽涌,有些常用的手段可以稍微簡(jiǎn)化代碼的復(fù)雜度平夜。

  • 使用“衛(wèi)語(yǔ)句(guard clause)”從函數(shù)中提前返回

    func Save(content string)bool{
        if len(content) == 0 {
            return false
        }
        // 很多其他判斷條件
        ...
    }
    
  • 最小化嵌套

    if userResult == SUCCESS{
        if permissionResult != SUCCESS{
            reply.Error("error..")
            reply.Done
            return;
        }
        reply.Error("")
    }else{
        reply.Error(userResult)
    }
    reply.Done()
    

    上面代碼 if ... else 嵌套對(duì)理解并沒(méi)增加什么困難,但是當(dāng)在一個(gè)復(fù)雜函數(shù)中時(shí)卸亮,理解這個(gè)嵌套就比較吃力忽妒,我們可以提前返回,調(diào)整代碼順序兼贸,理解起來(lái)更容易段直。

    if userResult != SUCCESS{
        reply.Error("error..")
        reply.Done()
        return;
    }
    if permissionResult != SUCCESS{
        reply.Error("error..")
        reply.Done()
        return;
    }
    reply.Error("")
    reply.Done()
    

    在循環(huán)中出現(xiàn)嵌套我們也可以采取提前返回的方法,只是把return換成breakcontinue溶诞。

    for _, item := range results{
      if item != nil{
        // do ...
        if item.name != ""{
          // do...
        }
      }
    }
    

    采取條件提前返回鸯檬,調(diào)整代碼后:

    for _, item := range results{
      if item == nil{
        continue
      }
      if item.name == ""{
        continue
      }
      // do ...
    }
    

    以上就是代碼規(guī)范相關(guān)的幾點(diǎn)內(nèi)容,沒(méi)多少難度卻意義重大螺垢。


代碼重構(gòu)的方式

一幢有少許破窗的建筑為例京闰,如果那些窗不被修理好,可能將會(huì)有破壞者破壞更多的窗戶(hù)甩苛。最終他們甚至?xí)J入建筑內(nèi),如果發(fā)現(xiàn)無(wú)人居住俏站,也許就在那里定居或者縱火讯蒲。一面墻,如果出現(xiàn)一些涂鴉沒(méi)有被清洗掉肄扎,很快的墨林,墻上就布滿(mǎn)了亂七八糟赁酝、不堪入目的東西;一條人行道有些許紙屑旭等,不久后就會(huì)有更多垃圾酌呆,最終人們會(huì)視若理所當(dāng)然地將垃圾順手丟棄在地上。這個(gè)現(xiàn)象搔耕,就是犯罪心理學(xué)中的破窗效應(yīng)隙袁。

我們的代碼也是一樣,一段優(yōu)秀的代碼弃榨,如果從小到變量命名開(kāi)始不注意菩收,就會(huì)開(kāi)始“腐爛”,直到自己都忍受不了鲸睛,最后推倒重來(lái)娜饵,開(kāi)始惡性循環(huán)。因此官辈,我們需要經(jīng)常對(duì)代碼進(jìn)行重構(gòu)箱舞,不斷改善軟件設(shè)計(jì),使軟件更容易理解拳亿,保持軟件的“生命力”晴股。

何為重構(gòu)?

在不改變代碼外在行為的前提下风瘦,對(duì)代碼做出修改队魏,以改進(jìn)程序的內(nèi)部結(jié)構(gòu)。本質(zhì)上說(shuō)万搔,重構(gòu)就是在代碼寫(xiě)好之后改進(jìn)它的設(shè)計(jì)胡桨,這個(gè)改進(jìn)會(huì)讓代碼更容易理解,更易于修改瞬雹,而這可能使程序運(yùn)行得更快昧谊,也可能使程序運(yùn)行得更慢。它融入到整個(gè)開(kāi)發(fā)過(guò)程酗捌,而不是需要專(zhuān)門(mén)的時(shí)間呢诬,需要專(zhuān)門(mén)時(shí)間來(lái)寫(xiě)那是“重寫(xiě)”,而不是“重構(gòu)”胖缤。

重構(gòu)時(shí)機(jī)

重構(gòu)是融入到整個(gè)開(kāi)發(fā)過(guò)程的尚镰,上一分鐘在寫(xiě)代碼,下一分鐘可能就在重構(gòu)哪廓。當(dāng)出現(xiàn)下面一些時(shí)機(jī)狗唉,就可以考慮重構(gòu)了。

  • 新添加功能

    每次在新添加功能之前涡真,考慮一下是不是把當(dāng)前代碼重構(gòu)一下分俯,更容易添加這個(gè)功能肾筐。

    每次要修改時(shí),首先令修改很容易(警告:這件事有時(shí)會(huì)很難)缸剪,然后再進(jìn)行這次容易的修改吗铐。

  • 重復(fù)代碼——DRY

    當(dāng)看到重復(fù)代碼時(shí),毫不猶豫進(jìn)行重構(gòu)杏节。

  • 過(guò)時(shí)的知識(shí)

    例如當(dāng)一個(gè)語(yǔ)言出現(xiàn)新特性唬渗,發(fā)現(xiàn)這個(gè)特性能夠使代碼更簡(jiǎn)潔,更優(yōu)雅時(shí)拢锹,可以考慮重構(gòu)谣妻。

  • 一個(gè)小的重構(gòu)是否能使代碼更容易理解

有個(gè)通用的原則就是,保證每次離開(kāi)你所見(jiàn)到的代碼時(shí)卒稳,比你剛看到時(shí)更漂亮蹋半。當(dāng)然也有不需要重構(gòu)的情況,例如有些運(yùn)行良好的底層“祖?zhèn)鞔a”充坑,已經(jīng)成功運(yùn)行N年沒(méi)出現(xiàn)過(guò)bug减江,很少人動(dòng)的代碼,那就不要重構(gòu)了捻爷,不能保證你重構(gòu)的代碼比現(xiàn)在更好辈灼。而有些代碼發(fā)現(xiàn)重寫(xiě)比重構(gòu)更容易時(shí),也不要去重構(gòu)了也榄,直接重寫(xiě)吧巡莹。

在重構(gòu)之前,一定要確保有良好的測(cè)試甜紫,在這個(gè)前提下用簡(jiǎn)短又慎重的步驟進(jìn)行降宅,一次修改一個(gè)小點(diǎn),然后頻繁的運(yùn)行測(cè)試用例囚霸,保證每次的重構(gòu)都能通過(guò)所有測(cè)試腰根,不影響系統(tǒng)正常運(yùn)行。

“壞味道”與技巧

識(shí)別出代碼中的“壞味道”拓型,然后用一些重構(gòu)技巧來(lái)優(yōu)化额嘿,下面列舉一些典型的“壞味道”和常用的重構(gòu)技巧。

  • 神秘的命名

    正如本文代碼風(fēng)格中關(guān)于命名的描述劣挫,遇到令人費(fèi)解的變量册养、字段或命名時(shí),對(duì)它們進(jìn)行改名操作⊙构蹋現(xiàn)在IDE都支持一鍵改名操作捕儒。

  • 重復(fù)代碼

    重復(fù)代碼的危害眾所周知,修改一處代碼的時(shí)候還需要兼顧其他代碼。

    改變代碼的順序刘莹,重組代碼,把重復(fù)代碼提煉成函數(shù)焚刚。

  • 過(guò)長(zhǎng)的函數(shù)

    每當(dāng)感覺(jué)某段代碼需要注釋來(lái)說(shuō)明的時(shí)候点弯,可以把要說(shuō)明的東西寫(xiě)進(jìn)一個(gè)獨(dú)立的函數(shù)中,并以其用途命名矿咕。我們可以對(duì)一組甚至短短一行代碼做這件事抢肛。哪怕替換后的函數(shù)調(diào)用動(dòng)作比函數(shù)自身還長(zhǎng),只要函數(shù)名稱(chēng)能夠解釋其用途碳柱,我們也該毫不猶豫地那么做捡絮。關(guān)鍵不在于函數(shù)的長(zhǎng)度,而在于函數(shù)“做什么”和“如何做”之間的語(yǔ)義距離莲镣。

  • 參數(shù)列表過(guò)長(zhǎng)

    引入?yún)?shù)對(duì)象福稳,將相關(guān)的參數(shù)用一個(gè)對(duì)象封裝起來(lái)。

    func Save(name, path, perm string, length int, flag int ...)
    

    有些函數(shù)有甚至上十個(gè)參數(shù)瑞侮,調(diào)用的時(shí)候容易傳參錯(cuò)誤的圆,可以定義一個(gè)對(duì)象,調(diào)用時(shí)傳遞該對(duì)象即可半火。

    type Param struct{
      name string
      path string 
      perm string
      length int
      flag int 
      ...
    }
    func Save(data Param)
    
  • 全局變量

    全局變量的危害在于越妈,當(dāng)不止一處代碼會(huì)對(duì)其進(jìn)行修改,往往會(huì)造成不可預(yù)期的結(jié)果钮糖。

    我們可以采用單例模式或著封裝變量梅掠,控制全局變量的作用域,對(duì)外只提供操作的接口店归。

    var global *Asset
    
    func SetAssetName(name string){
      global.Name = name
    }
    
  • 發(fā)散式變化

    當(dāng)我們修改或增加一處功能時(shí)阎抒,必須同時(shí)修改若干個(gè)函數(shù),這叫做發(fā)散式變化娱节∧域龋可以提煉和搬移函數(shù),做到“每次只關(guān)心一個(gè)上下文”肄满,將相關(guān)函數(shù)搬到一起谴古。

  • 依戀情節(jié)

    一個(gè)函數(shù)跟另一個(gè)模塊中的函數(shù)或者數(shù)據(jù)交流格外頻繁,遠(yuǎn)勝于在自己所處模塊內(nèi)部的交流稠歉,這就是依戀情結(jié)的典型情況掰担。

    判斷哪個(gè)模塊擁有的此函數(shù)使用的數(shù)據(jù)最多,然后就把這個(gè)函數(shù)和那些數(shù)據(jù)擺在一起怒炸,將那些總是一起變化的東西放在一塊带饱。

  • 數(shù)據(jù)泥團(tuán)

    將分散的變量匯總起來(lái),為數(shù)據(jù)泥團(tuán)新建一個(gè)類(lèi),不要擔(dān)心只用到了其中幾個(gè)字段勺疼。

    type PermData struct{
      path string
      name string
      perm string
      ...
    }
    
  • 過(guò)長(zhǎng)的消息鏈(調(diào)用鏈)

    過(guò)長(zhǎng)的消息鏈教寂,一旦當(dāng)其中一個(gè)對(duì)象發(fā)生修改,我們就不得不更改所有地方执庐。

    name = company().section().user().name()
    

    增加一個(gè)“中間者”酪耕,減少消息鏈。

  • 過(guò)大的類(lèi)

    觀察一個(gè)大類(lèi)的使用者轨淌,經(jīng)常能找到如何拆分類(lèi)的線(xiàn)索迂烁。看看使用者是否只用到了這個(gè)類(lèi)所有功能的一個(gè)子集递鹉,每個(gè)這樣的子集都可能拆分成一個(gè)獨(dú)立的類(lèi)盟步。


單元測(cè)試

有人說(shuō),看一段代碼質(zhì)量如何躏结,就看代碼中的接口多不多却盘。一個(gè)接口都沒(méi)有的代碼,很難說(shuō)是好代碼窜觉,至少很難測(cè)試谷炸。在敏捷開(kāi)發(fā)中,有一種TDD方案禀挫,在正式寫(xiě)代碼之前旬陡,先寫(xiě)單元測(cè)試,然后再讓單元測(cè)試通過(guò)语婴,

這種方式能夠讓代碼更優(yōu)雅描孟,更簡(jiǎn)潔,當(dāng)然需要的開(kāi)發(fā)時(shí)間也很多砰左。理想是美好的匿醒,現(xiàn)實(shí)情況是,由于各種原因缠导,不可能?chē)?yán)格按照TDD所提倡的方式來(lái)進(jìn)行開(kāi)發(fā)廉羔。但是核心代碼的單元測(cè)試必不可少,只有在有單元測(cè)試覆蓋的情況下僻造,我們才能放心的進(jìn)行重構(gòu)與開(kāi)發(fā)憋他。

一個(gè)好的單元測(cè)試要求運(yùn)行時(shí)間短、可以幫忙快速的定位問(wèn)題髓削,試想一下竹挡,如果一個(gè)單元測(cè)試用例需要運(yùn)行1s,那么整個(gè)項(xiàng)目上千個(gè)測(cè)試用例耗時(shí)難以忍受立膛,我們更不愿意頻繁的跑測(cè)試揪罕。

有時(shí)候梯码,我們會(huì)寫(xiě)一些測(cè)試代碼,來(lái)測(cè)試功能是否通過(guò)好啰,但有時(shí)這些不能稱(chēng)之為單元測(cè)試轩娶,因?yàn)橥@些臨時(shí)寫(xiě)的測(cè)試代碼,有下面這些特點(diǎn):

  • 依賴(lài)數(shù)據(jù)庫(kù)
  • 依賴(lài)網(wǎng)絡(luò)通信
  • 調(diào)用文件系統(tǒng)
  • 需要額外的配置文件

下面簡(jiǎn)單介紹一些單元測(cè)試中常用到的框架坎怪。

goconvey

雖然 Golang 自帶了單元測(cè)試功能和很多其他第三方測(cè)試框架罢坝,但是 GoConvey 更簡(jiǎn)潔,更優(yōu)雅搅窿,使用起來(lái)更方便。

我們通過(guò)一個(gè)案例來(lái)介紹如何使用隙券。

待測(cè)試函數(shù):

func CompareSlice(a, b []int) bool{
    if len(a) != len(b){
        return false
    }

    if (a == nil) != (b == nil){
        return false
    }
    for i, v  := range a{
        if v != b[i]{
            return false
        }
    }
    return true
}

測(cè)試代碼:

func TestCompareSlice(t *testing.T) {
    Convey("TestSliceEqual", t, func() {
        Convey("should be true", func() {
            a := []int{1, 2, 3}
            b := []int{1, 2, 3}
            So(CompareSlice(a, b), ShouldBeTrue)
        })
        Convey("should return true a=nil&b=nil", func() {
            So(CompareSlice(nil, nil), ShouldBeTrue)
        })
        Convey("should return false", func() {
            a := []int{1, 2, 3}
            b := []int{1, 2}
            So(CompareSlice(a,b), ShouldBeFalse)
        })
    })
}

最常用的函數(shù)就是通過(guò)So 進(jìn)行斷言男应。

輸出:

=== RUN   TestCompareSlice

  TestSliceEqual
    should be true ?
    should return true a=nil&b=nil ?
    should return false ?


3 total assertions

--- PASS: TestCompareSlice (0.00s)

測(cè)試失敗的情況:

=== RUN   TestCompareSlice2

  should be true ?


1 total assertion


  should return true a=nil&b=nil ?


2 total assertions


  should return false ?


Failures:

  * /Users/jiami/go/src/awesome/example_test.go
  Line 44:
  Expected: false
  Actual:   true
  goroutine 7 [running]:

使用時(shí)有如下幾個(gè)要點(diǎn):

1、import goconvey包時(shí)娱仔,前面加"."號(hào)沐飘,以減少冗余代碼。

2牲迫、測(cè)試函數(shù)名稱(chēng)以Test開(kāi)頭耐朴,參數(shù)是*testing.T

3盹憎、每個(gè)測(cè)試用例必須用Convey函數(shù)包裹起來(lái)筛峭。

gomoke

當(dāng)待測(cè)試的函數(shù)/對(duì)象的依賴(lài)關(guān)系很復(fù)雜,并且有些依賴(lài)不能直接創(chuàng)建陪每,例如數(shù)據(jù)庫(kù)連接影晓、文件I/O等。這種場(chǎng)景就非常適合使用 mock/stub 測(cè)試檩禾。簡(jiǎn)單來(lái)說(shuō)挂签,就是用 mock 對(duì)象模擬依賴(lài)項(xiàng)的行為。

gomoke是官方提供的moke框架盼产,下面將用一個(gè)簡(jiǎn)單的例子來(lái)介紹如何使用饵婆。

// 文件名: db.go
type DB interface {
    Get(key string)(int, error)
}

func GetFromDB(db DB, key string)int{
    if value, err := db.Get(key); err == nil{
        return value
    }
    return -1
}

在測(cè)試用例中,不能創(chuàng)建真正的數(shù)據(jù)庫(kù)連接戏售,這個(gè)時(shí)候我們就可以moke DB 接口侨核。

首先生成 moke 對(duì)象:

mockgen -source=db.go -destination=db_mock.go -package=main

此時(shí),就會(huì)生成一個(gè)名為 db_moke.go 的文件蜈项,部分代碼如下:

// MockDB is a mock of DB interface
type MockDB struct {
    ctrl     *gomock.Controller
    recorder *MockDBMockRecorder
}

// MockDBMockRecorder is the mock recorder for MockDB
type MockDBMockRecorder struct {
    mock *MockDB
}

// NewMockDB creates a new mock instance
func NewMockDB(ctrl *gomock.Controller) *MockDB {
    mock := &MockDB{ctrl: ctrl}
    mock.recorder = &MockDBMockRecorder{mock}
    return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockDB) EXPECT() *MockDBMockRecorder {
    return m.recorder
}

// Get mocks base method
func (m *MockDB) Get(key string) (int, error) {
    ret := m.ctrl.Call(m, "Get", key)
    ret0, _ := ret[0].(int)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}
...

接下來(lái)芹关,在我們的測(cè)試用例中就可以使用 MockDB 來(lái)模擬DB操作了,測(cè)試代碼如下:

func TestGetFromDB(t *testing.T) {
    Convey("test mock",t, func() {
        ctrl := gomock.NewController(t)
        defer ctrl.Finish()


        m := NewMockDB(ctrl)
        m.EXPECT().Get(gomock.Eq("Test")).Return(0, errors.New("not exist"))
        So(GetFromDB(m, "Test"), ShouldEqual, -1)

        m.EXPECT().Get(gomock.Any()).DoAndReturn(func(key string) (int, error){
            if key == "test"{
                return 1, nil
            }
            return -1, errors.New("not exist")
        }).AnyTimes()
        So(GetFromDB(m, "test"), ShouldEqual, 1)
        So(GetFromDB(m, "test1"), ShouldEqual, -1)
    })
}

我們可以使用EXCEPT()函數(shù)來(lái)“打樁”——給定明確的參數(shù)和返回值紧卒、檢測(cè)調(diào)用次數(shù)侥衬、調(diào)用順序,動(dòng)態(tài)設(shè)置返回值等。

gomonkey

這是國(guó)人寫(xiě)的一個(gè)很方便的打樁框架轴总,接口友好直颅,功能強(qiáng)大,支持下面一些特性:

  • 支持為一個(gè)函數(shù)打一個(gè)樁
  • 支持為一個(gè)成員方法打一個(gè)樁
  • 支持為一個(gè)全局變量打一個(gè)樁
  • 支持為一個(gè)函數(shù)變量打一個(gè)樁
  • 支持為一個(gè)函數(shù)打一個(gè)特定的樁序列
  • 支持為一個(gè)成員方法打一個(gè)特定的樁序列
  • 支持為一個(gè)函數(shù)變量打一個(gè)特定的樁序列

下面就“支持為一個(gè)函數(shù)打一個(gè)樁”怀樟、“支持為一個(gè)成員方法打一個(gè)樁”功偿、“支持為一個(gè)全局變量打一個(gè)樁”來(lái)舉例說(shuō)明如何使用,具體功能有興趣可自行搜索往堡。

為一個(gè)函數(shù)打一個(gè)樁

需要用到下面一個(gè)接口:

func ApplyFunc(target, double interface{}) *Patches
func (this *Patches) ApplyFunc(target, double interface{}) *Patches

ApplyFunc 第一個(gè)參數(shù)是函數(shù)名械荷,第二個(gè)參數(shù)是樁函數(shù)。測(cè)試完成后虑灰,patches 對(duì)象通過(guò) Reset 成員方法刪除所有測(cè)試樁吨瞎。
假設(shè)我們有一個(gè)用來(lái)比較大小的函數(shù):

func CompareInt(a,b int)int{
    if a == b{
        return 0
    }else if a > b{
        return 1
    }else{
        return -1
    }
}

我們需要為這個(gè)函數(shù)“打樁”,讓它在被調(diào)用時(shí)返回指定的結(jié)果穆咐,用法如下:

func TestCompareInt(t *testing.T) {
    Convey("CompareInt", t, func() {
        patch := ApplyFunc(CompareInt, func(_ int, _ int) int{
            return 0
        })
        defer patch.Reset()
        So(CompareInt(1,1), ShouldEqual, 0)
    })
}

設(shè)置完樁函數(shù)之后颤诀,我們?cè)俅握{(diào)用,無(wú)論參數(shù)是什么对湃,它都會(huì)返回 0 值崖叫。

這個(gè)也可以給標(biāo)準(zhǔn)庫(kù)中的函數(shù)進(jìn)行“打樁”,比如 strconv.FormatInt函數(shù):

func TestPackage(t *testing.T){
    Convey("test", t, func() {
        patch := ApplyFunc(strconv.FormatInt, func(_ int64, _ int)string{
            return "test_package"
        })
        defer patch.Reset()
        So(strconv.FormatInt(1,10), ShouldEqual, "test_package")
        Convey("test_2", func() {
            So(strconv.FormatInt(1,10), ShouldEqual, "test_package")
            patch.Reset()
            So(strconv.FormatInt(1,10), ShouldEqual, "1")
        })
    })
}

設(shè)置完樁函數(shù)后拍柒,strconv.FormatInt就不是原先的函數(shù)了心傀,它只會(huì)返回"test_package"。

為一個(gè)成員方法打一個(gè)樁

需要用到下面這個(gè)接口:

func ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches
func (this *Patches) ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches

例如斤儿,我們有個(gè) RedisCli 類(lèi)剧包,用來(lái)表示 redis 連接,其中有個(gè) Get 方法:

type RedisCli struct{
}

func (r *RedisCli)Get(key string)(string, error){
    return key + "test", nil
}

在單元測(cè)試中往果,不能真正的連接 redis疆液,為了測(cè)試代碼,我們除了可以 moke 外陕贮,還可以為 Get 方法打樁堕油,讓它暫時(shí)返回指定結(jié)果。

func TestClass(t *testing.T){
    Convey("test class", t, func() {
        var redisCli *RedisCli
        patch := ApplyMethod(reflect.TypeOf(redisCli), "Get", func(_ *RedisCli, _ string) (string, error){
            return "test_redis_get", nil
        })
        defer patch.Reset()

        redisCli = &RedisCli{}
        rsp, err := redisCli.Get("test")
        So(err, ShouldBeNil)
        So(rsp, ShouldEqual, "test_redis_get")
    })
}

注意:如果打樁目標(biāo)為內(nèi)聯(lián)的函數(shù)或成員方法肮之,請(qǐng)?jiān)跍y(cè)試時(shí)通過(guò)命令行參數(shù) -gcflags=-l 關(guān)閉內(nèi)聯(lián)優(yōu)化掉缺。

為一個(gè)全局變量打一個(gè)樁

需要用到下面一個(gè)接口:

func ApplyGlobalVar(target, double interface{}) *Patches
func (this *Patches) ApplyGlobalVar(target, double interface{}) *Patches

使用示例如下:

// 全局變量插樁
var num = 10
func TestApplyGlobalVar(t *testing.T){
    Convey("test global var", t, func() {
        Convey("change", func() {
            patch := ApplyGlobalVar(&num, 100)
            defer patch.Reset()

            So(num, ShouldEqual, 100)
        })

        Convey("recover", func() {
            So(num, ShouldEqual, 10)
        })
    })
}

總結(jié)

本文內(nèi)容只是作為一個(gè)簡(jiǎn)單的介紹,很多內(nèi)容被舍棄掉了戈擒,如果覺(jué)得太淺顯或太長(zhǎng)不看眶明,那么只需要在編碼時(shí)記得時(shí)刻問(wèn)自己下面兩個(gè)問(wèn)題:

  • 如果這樣寫(xiě)代碼,半年后的我會(huì)不會(huì)罵現(xiàn)在的自己筐高?
  • 這塊代碼怎樣寫(xiě)才方便測(cè)試搜囱?
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末丑瞧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蜀肘,更是在濱河造成了極大的恐慌绊汹,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扮宠,死亡現(xiàn)場(chǎng)離奇詭異西乖,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)坛增,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)获雕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人收捣,你說(shuō)我怎么就攤上這事典鸡。” “怎么了坏晦?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)嫁乘。 經(jīng)常有香客問(wèn)我昆婿,道長(zhǎng),這世上最難降的妖魔是什么蜓斧? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任仓蛆,我火速辦了婚禮,結(jié)果婚禮上挎春,老公的妹妹穿的比我還像新娘看疙。我一直安慰自己,他們只是感情好直奋,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布能庆。 她就那樣靜靜地躺著,像睡著了一般脚线。 火紅的嫁衣襯著肌膚如雪搁胆。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天邮绿,我揣著相機(jī)與錄音渠旁,去河邊找鬼。 笑死船逮,一個(gè)胖子當(dāng)著我的面吹牛顾腊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播挖胃,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼杂靶,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼梆惯!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起伪煤,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤加袋,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后抱既,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體职烧,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年防泵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蚀之。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捷泞,死狀恐怖足删,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锁右,我是刑警寧澤失受,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站咏瑟,受9級(jí)特大地震影響拂到,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜码泞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一兄旬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧余寥,春花似錦领铐、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至肥缔,卻和暖如春莲兢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背续膳。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工改艇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坟岔。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓谒兄,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親社付。 傳聞我的和親對(duì)象是個(gè)殘疾皇子承疲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

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