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

問:在遺留代碼上面折騰單元測試是什么樣的體驗?
答:有時候構(gòu)造一個最簡單的對象,給對象設最簡單的值都會讓你崩潰。。。

最近開始重構(gòu)一個項目的單元測試代碼,就有這種感覺,廢話不多說,先看一段測試代碼(注:為了不違反公司制度,本文的所有代碼都是經(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);
    }
}

這段測試代碼還是挺容易理解的,輸入一個Charge(費用)列表,輸出PaymentStatus(支付狀態(tài))。不管3721,運行一下再說,測試通過,今天運氣真不錯:)

測試雖然通過了,但是有兩個問題,第一個問題是準備Charge的代碼稍許有些重復,可以抽取一個方法用來構(gòu)造Charge對象,方法有兩個參數(shù),一個是total(總費用),一個是paid(已付費用),代碼如下,

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);
    }
}

改完代碼,立刻運行測試,順利通過,第一個問題解決。第二個問題比較嚴重,運行一個測試等了10s左右!

速度是檢驗單元測試的唯一標準!

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

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

原來時間都花在初始化配置和數(shù)據(jù)庫連接上了,而測試代碼并不需要讀取配置和數(shù)據(jù)庫訪問,于是立馬把extends BaseTestCase去掉,運行測試,立馬報錯,代碼果然不是說刪就能刪的。

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

靠,set方法也能報錯!看了一下setTotal方法,問題出在this.notifyChange()上面,notifyChange()是父類DomainObjectRoot(來自我司最著名的框架之一,但是對單元測試極不友好)的方法,需要訪問數(shù)據(jù)庫,剛才把初始化數(shù)據(jù)庫連接的代碼去掉了,所以出錯。

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

怎么才能不運行this.notifyChange()這行代碼呢?當然是用Mock大法(推薦mockito框架),在運行測試之前把它mock掉,測試代碼如下,

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

這下應該沒問題了吧,再運行測試,靠,一波還未平息,一波又來侵襲,又報錯!

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

暈,get方法也需要連數(shù)據(jù)庫!看到這里你會發(fā)現(xiàn)我司的單元測試是多喜歡連數(shù)據(jù)庫啊。讓我們一起看看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方法會根據(jù)id到數(shù)據(jù)庫里面把對應的PaymentStatus對象查出來。
那么怎么才能不調(diào)用PaymentStatusConst.getObj()呢?還是用Mock大法嗎?這里留個讀者自己思考一下,看看Mock是否可行?
我用了另外一種方法,其實,PaymentStatusConst和PaymentStatus是一一對應的,測試代碼完全可以只比較PaymentStatusConst,但是要先重構(gòu)產(chǎn)品代碼,把PaymentStatusCalculator.calculate(charges)拆成2個方法,把討厭的getObj方法扔進另外一個傻瓜的不需要測試的方法中,代碼如下:

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

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

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

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);
    }
}

改完代碼,運行測試,通過而且只用了0.53s,到此終于折騰完畢。

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

  1. 運行測試,讓測試通過。
  2. 消除明顯的重復,讓代碼更少更清晰。
  3. 利用Mock繞過讓單元測試變慢的代碼。
  4. 很難Mock的時候,通過重構(gòu)生產(chǎn)代碼繞過讓測試變慢的代碼。
  5. 運行測試,驗證你的改動。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,007評論 25 709
  • @Author:彭海波 前言 單元測試(又稱為模塊測試, Unit Testing)是針對程序模塊(軟件設計的最小...
    海波筆記閱讀 5,108評論 0 52
  • 2017.10.18 發(fā)現(xiàn)最近我能較快地調(diào)整情緒,緩解焦慮了。真是太棒了! 我用到的方法如下: 1. ...
    amylismile閱讀 366評論 0 0
  • 早在2016還沒來得及到來,2015剛要結(jié)束的時候,自己就開始被所謂的“本命年”嚇慘了。24了,真真實實的本命年啊...
    桃子很甜閱讀 369評論 0 0
  • 鄉(xiāng)居數(shù)年,花開四季,鳥獸百態(tài),氣象萬千。人移新居,魂牽舊籬。朝朝頻顧,故友難舍。 狂壑籠晴嵐,霞生云風殘。 故園訪...
    蔚海山莊三六子閱讀 301評論 0 2

友情鏈接更多精彩內(nèi)容