一次單元測(cè)試代碼的重構(gòu)之旅

問:在遺留代碼上面折騰單元測(cè)試是什么樣的體驗(yàn)明吩?
答:有時(shí)候構(gòu)造一個(gè)最簡(jiǎn)單的對(duì)象,給對(duì)象設(shè)最簡(jiǎn)單的值都會(huì)讓你崩潰殷费。印荔。低葫。

最近開始重構(gòu)一個(gè)項(xiàng)目的單元測(cè)試代碼,就有這種感覺仍律,廢話不多說嘿悬,先看一段測(cè)試代碼(注:為了不違反公司制度壹无,本文的所有代碼都是經(jīng)過加工過的代碼)

public class PaymentStatusTest  extends BaseTestCase  {
    @Test
    public void testPaymentStatus() {
        List<Charge> charges = Lists.newArrayList();
        Charge c1 = new Charge();
        c1.setTotal(100.0);
        c1.setPaid(50.0);
        charges.add(c1);
        Charge c2 = new Charge();
        c2.setTotal(100.0);
        c2.setPaid(100.0);
        charges.add(c2);
        PaymentStatus status = PaymentStatusCalculator.calculate(charges);
        assertEquals(PaymentStatusConst.PARTIAL_PAID.getObj(), status);
    }
}

這段測(cè)試代碼還是挺容易理解的云矫,輸入一個(gè)Charge(費(fèi)用)列表,輸出PaymentStatus(支付狀態(tài))啃奴。不管3721草则,運(yùn)行一下再說钢拧,測(cè)試通過,今天運(yùn)氣真不錯(cuò):)

測(cè)試雖然通過了炕横,但是有兩個(gè)問題娶靡,第一個(gè)問題是準(zhǔn)備Charge的代碼稍許有些重復(fù),可以抽取一個(gè)方法用來構(gòu)造Charge對(duì)象看锉,方法有兩個(gè)參數(shù)姿锭,一個(gè)是total(總費(fèi)用),一個(gè)是paid(已付費(fèi)用)伯铣,代碼如下呻此,

public class PaymentStatusTest extends BaseTestCase {
    @Test
    public void testPaymentStatus() {
        List<Charge> charges = Lists.newArrayList();
        charges.add(createCharge(100, 50));
        charges.add(createCharge(100, 100));
        PaymentStatus status = PaymentStatusCalculator.calculate(charges);
        assertEquals(PaymentStatusConst.PARTIAL_PAID.getObj(), status);
    }
}

改完代碼,立刻運(yùn)行測(cè)試腔寡,順利通過焚鲜,第一個(gè)問題解決。第二個(gè)問題比較嚴(yán)重放前,運(yùn)行一個(gè)測(cè)試等了10s左右忿磅!

速度是檢驗(yàn)單元測(cè)試的唯一標(biāo)準(zhǔn)!

一個(gè)10s級(jí)別的單元測(cè)試是完全不可接受的,于是開始檢查慢的原因凭语,發(fā)現(xiàn)問題出在了BaseTestCase

public class BaseTestCase {
    @BeforeClass
    public static void before() {
        loadFwConfig();
        initDBConnection();
    }
}

原來時(shí)間都花在初始化配置和數(shù)據(jù)庫(kù)連接上了葱她,而測(cè)試代碼并不需要讀取配置和數(shù)據(jù)庫(kù)訪問,于是立馬把extends BaseTestCase去掉似扔,運(yùn)行測(cè)試吨些,立馬報(bào)錯(cuò),代碼果然不是說刪就能刪的炒辉。

java.lang.ExceptionInInitializerError
    at com.goldtalent.DomainObjectRoot.notifyChange(DomainObjectRoot.java:242)
    at com.goldtalent.Charge.setTotal(Charge.java:17)

靠豪墅,set方法也能報(bào)錯(cuò)!看了一下setTotal方法黔寇,問題出在this.notifyChange()上面偶器,notifyChange()是父類DomainObjectRoot(來自我司最著名的框架之一,但是對(duì)單元測(cè)試極不友好)的方法,需要訪問數(shù)據(jù)庫(kù)屏轰,剛才把初始化數(shù)據(jù)庫(kù)連接的代碼去掉了颊郎,所以出錯(cuò)。

public class Charge extends DomainObjectRoot { 
    public void setTotal(Double total) {
        this.total = total;
        this.notifyChange();
    }
}

怎么才能不運(yùn)行this.notifyChange()這行代碼呢亭枷?當(dāng)然是用Mock大法(推薦mockito框架)袭艺,在運(yùn)行測(cè)試之前把它mock掉,測(cè)試代碼如下叨粘,

public class PaymentStatusTest {
    @BeforeClass
    public static void before() {
        new MockUp<DomainObjectRoot>() {
            @Mock
            public void notifyChange() {
            //哈哈猾编,這下你訪問不了數(shù)據(jù)庫(kù)了吧!
            }
        };
    }
}

這下應(yīng)該沒問題了吧升敲,再運(yùn)行測(cè)試答倡,靠,一波還未平息驴党,一波又來侵襲瘪撇,又報(bào)錯(cuò)!

java.lang.ExceptionInInitializerError
    at com.goldtalent.DBUtils.getEntityManager(DBUtils.java:17)
    at com.goldtalent.PaymentStatusConst.getObj(PaymentStatusConst.java:33)

暈港庄,get方法也需要連數(shù)據(jù)庫(kù)倔既!看到這里你會(huì)發(fā)現(xiàn)我司的單元測(cè)試是多喜歡連數(shù)據(jù)庫(kù)啊。讓我們一起看看PaymentStatusConst

public class PaymentStatusConst {
    private long id;
    public PaymentStatusConst(long id) {
        this.id = id;
    }
    public PaymentStatus getObj() {
        EntityManager em = DBUtils.getEntityManager();
        return (PaymentStatus) em.find(PaymentStatus.class, this.id);
    }
}

PaymentStatusConst.getObj方法會(huì)根據(jù)id到數(shù)據(jù)庫(kù)里面把對(duì)應(yīng)的PaymentStatus對(duì)象查出來鹏氧。
那么怎么才能不調(diào)用PaymentStatusConst.getObj()呢渤涌?還是用Mock大法嗎?這里留個(gè)讀者自己思考一下把还,看看Mock是否可行实蓬?
我用了另外一種方法,其實(shí)吊履,PaymentStatusConst和PaymentStatus是一一對(duì)應(yīng)的安皱,測(cè)試代碼完全可以只比較PaymentStatusConst,但是要先重構(gòu)產(chǎn)品代碼艇炎,把PaymentStatusCalculator.calculate(charges)拆成2個(gè)方法酌伊,把討厭的getObj方法扔進(jìn)另外一個(gè)傻瓜的不需要測(cè)試的方法中,代碼如下:

   public static PaymentStatusConst calculatePaymentStatusConst(List<Charge> charges) {
       //這里省略計(jì)算總費(fèi)用和總支付費(fèi)用的邏輯 
       if (paidAmount == 0) {
            return PaymentStatusConst.NONE;
        if (paidAmount - totalAmount >= 0) {
            return PaymentStatusConst.FULLY_PAID;
        } 
        return PaymentStatusConst.PARTIAL_PAID;
    }

    //這個(gè)方法簡(jiǎn)單到?jīng)]邏輯冕臭,所以不用測(cè)試
    public static PaymentStatus calculate(List<Charge> charges) {
        return calculatePaymentStatusConst(charges).getObj();
    }

重構(gòu)完的測(cè)試代碼如下:

public class PaymentStatusTest extends BaseTestCase {
    @Test
    public void testPaymentStatus() {
        List<Charge> charges = Lists.newArrayList();
        charges.add(createCharge(100, 50));
        charges.add(createCharge(100, 100));
        PaymentStatusConst status = PaymentStatusCalculator.calculatePaymentStatusConst(charges);
        //getObj()消失了腺晾,哈哈
        assertEquals(PaymentStatusConst.PARTIAL_PAID, status);
    }
}

改完代碼,運(yùn)行測(cè)試辜贵,通過而且只用了0.53s,到此終于折騰完畢归形。

回顧下這次重構(gòu)托慨,主要經(jīng)歷了下面幾個(gè)步驟,

  1. 運(yùn)行測(cè)試暇榴,讓測(cè)試通過厚棵。
  2. 消除明顯的重復(fù)蕉世,讓代碼更少更清晰。
  3. 利用Mock繞過讓單元測(cè)試變慢的代碼婆硬。
  4. 很難Mock的時(shí)候狠轻,通過重構(gòu)生產(chǎn)代碼繞過讓測(cè)試變慢的代碼。
  5. 運(yùn)行測(cè)試彬犯,驗(yàn)證你的改動(dòng)向楼。

寫出真正的單元測(cè)試不容易,你是否也曾經(jīng)碰到過重重的障礙谐区?不要放棄湖蜕,相信我們終將能馴服單元測(cè)試。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宋列,一起剝皮案震驚了整個(gè)濱河市昭抒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌炼杖,老刑警劉巖灭返,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異坤邪,居然都是意外死亡熙含,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門罩扇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來婆芦,“玉大人,你說我怎么就攤上這事喂饥∠迹” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵员帮,是天一觀的道長(zhǎng)或粮。 經(jīng)常有香客問我,道長(zhǎng)捞高,這世上最難降的妖魔是什么氯材? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮硝岗,結(jié)果婚禮上氢哮,老公的妹妹穿的比我還像新娘。我一直安慰自己型檀,他們只是感情好冗尤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般裂七。 火紅的嫁衣襯著肌膚如雪皆看。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天背零,我揣著相機(jī)與錄音腰吟,去河邊找鬼。 笑死徙瓶,一個(gè)胖子當(dāng)著我的面吹牛毛雇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播倍啥,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼禾乘,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了虽缕?” 一聲冷哼從身側(cè)響起始藕,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎氮趋,沒想到半個(gè)月后伍派,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡剩胁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年诉植,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昵观。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡晾腔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出啊犬,到底是詐尸還是另有隱情灼擂,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布觉至,位于F島的核電站剔应,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏语御。R本人自食惡果不足惜峻贮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望应闯。 院中可真熱鬧纤控,春花似錦、人聲如沸碉纺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)惜辑。三九已至唬涧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間盛撑,已是汗流浹背碎节。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抵卫,地道東北人狮荔。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像介粘,于是被迫代替她去往敵國(guó)和親殖氏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,144評(píng)論 25 707
  • @Author:彭海波 前言 單元測(cè)試(又稱為模塊測(cè)試, Unit Testing)是針對(duì)程序模塊(軟件設(shè)計(jì)的最小...
    海波筆記閱讀 4,961評(píng)論 0 52
  • 2017.10.18 發(fā)現(xiàn)最近我能較快地調(diào)整情緒姻采,緩解焦慮了雅采。真是太棒了! 我用到的方法如下: 1. ...
    amylismile閱讀 285評(píng)論 0 0
  • 早在2016還沒來得及到來,2015剛要結(jié)束的時(shí)候刑棵,自己就開始被所謂的“本命年”嚇慘了巴刻。24了,真真實(shí)實(shí)的本命年啊...
    桃子很甜閱讀 346評(píng)論 0 0
  • 鄉(xiāng)居數(shù)年蛉签,花開四季胡陪,鳥獸百態(tài),氣象萬(wàn)千碍舍。人移新居柠座,魂?duì)颗f籬。朝朝頻顧乒验,故友難舍愚隧。 狂壑籠晴嵐,霞生云風(fēng)殘锻全。 故園訪...
    蔚海山莊三六子閱讀 244評(píng)論 0 2