單元測(cè)試的一些分享

背景

最近在給一個(gè)客戶做技術(shù)咨詢,然后發(fā)現(xiàn)了客戶對(duì)于單元測(cè)試的一個(gè)有意思的現(xiàn)象。分享出來(lái),大家一起學(xué)習(xí)探討一下。

現(xiàn)狀分析

這里以java后端項(xiàng)目例,發(fā)現(xiàn)客戶寫的測(cè)試長(zhǎng)下面的樣子。(代碼已經(jīng)脫敏處理過(guò)。)

    @Autowired
    private SampleJob handler;

    @Test
    public void testStart() throws Exception {
        SampleParamVo paramVo = new SampleParamVo();
        paramVo.setStartTime("2021-03-18");
        paramVo.setEndTime("2021-03-18");
        handler.execute(paramVo);
    }

    @Autowired
    private SampleHandler handler;

    @Test
    public void testHandler() {
        handler.doHandler(new DateTime("2021-11-26"), null);
    }

那么這樣的測(cè)試代碼有什么問(wèn)題呢?

  1. 別人看不懂這個(gè)測(cè)試是在做什么。首先測(cè)試的方法名沒(méi)有任何意義,其次測(cè)試代碼也只是調(diào)用了某個(gè)函數(shù)。
  2. 無(wú)法運(yùn)行。這類測(cè)試代碼運(yùn)行往往需要啟動(dòng)其他服務(wù)或者需要一些特殊的設(shè)置。無(wú)法運(yùn)行就意味著它不能成為CI跑測(cè)試的一部分。
  3. 沒(méi)有斷言。沒(méi)有斷言就無(wú)法知道測(cè)試的代碼的正確性。
  4. 使用了@Autowired這樣的代碼,增加了測(cè)試的耦合以及編寫成本。

和客戶深聊了之后發(fā)現(xiàn),原來(lái)客戶不同的人對(duì)單元測(cè)試的理解也不一樣。

  • 寫這個(gè)代碼的開(kāi)發(fā)人員說(shuō),“這些代碼是在開(kāi)發(fā)完成之后做一些自測(cè)的輔助腳本?!?/li>
  • 有的開(kāi)發(fā)人員說(shuō),“我們是微服務(wù),單元測(cè)試需要調(diào)用其他服務(wù),寫起來(lái)很麻煩,而且如果其他服務(wù)不可用時(shí),測(cè)試也跑不過(guò)。”
  • 測(cè)試人員說(shuō):“單元測(cè)試我們有的,我每天都在寫測(cè)試用例,到單元測(cè)試的時(shí)候我就會(huì)把我的用例全部過(guò)一遍。”

所以我們可以發(fā)現(xiàn),有的開(kāi)發(fā)人員口中的單元測(cè)試其實(shí)應(yīng)該屬于集成測(cè)試或者E2E測(cè)試,有的開(kāi)發(fā)人員完全沒(méi)有寫過(guò)單元測(cè)試,而測(cè)試人員理解單元測(cè)試是自己手動(dòng)測(cè)試的時(shí)候用的測(cè)試用例。

那我們就先來(lái)說(shuō)說(shuō)什么是單元測(cè)試。

什么是單元測(cè)試?

單元測(cè)試(unit testing),是指對(duì)軟件中的最小可測(cè)試單元進(jìn)行檢查和驗(yàn)證。

通常在java的世界里面,單元測(cè)試就是指對(duì)一個(gè)public的方法編寫檢查和驗(yàn)證的代碼。

為什么要寫單元測(cè)試?

寫單元測(cè)試主要有兩大目的:

  1. 驗(yàn)證功能實(shí)現(xiàn)。
  2. 保護(hù)已有功能不被破壞。

當(dāng)我們寫完一個(gè)方法,我們?nèi)绾沃雷约簩懙姆椒ㄊ前雌谕ぷ鞯哪??這個(gè)時(shí)候就可以添加單元測(cè)試來(lái)驗(yàn)證我們的代碼是按期望工作的。即當(dāng)我們給定指定的輸入,我們獲得期望的輸出,則我們說(shuō)這個(gè)功能是符合期望的。

其次,代碼不是寫了就永遠(yuǎn)不變的,當(dāng)需求變更時(shí),新增需求時(shí),修復(fù)bug時(shí),都會(huì)修改代碼,而單元測(cè)試則能保護(hù)我們已有的功能不被破壞。保護(hù)已有功能不會(huì)被自己破壞,被新人破壞,被新功能破壞。

如何寫單元測(cè)試?

下面是一個(gè)單元測(cè)試的例子

    @Test
    public void should_return_fizz_given_input_can_be_divided_by_3() {
        FizzBuzz fizzBuzz = new FizzBuzz(); // Given
        String actual = fizzBuzz.sayIt(6); // When
    Assertions.assertEquals("Fizz", actual); // Then
    }

一個(gè)標(biāo)準(zhǔn)的單元測(cè)試包含以下幾個(gè)部分:

  1. 能描述清楚做了什么的測(cè)試名(方法名)
  2. 單元測(cè)試的Given、When、Then具體內(nèi)容。
    1. Given:初始狀態(tài)或前置條件
    2. When:行為發(fā)生
    3. Then:斷言結(jié)果

寫好單元測(cè)試要主要幾個(gè)要點(diǎn):

  • 因?yàn)闇y(cè)試代碼并不會(huì)進(jìn)入生產(chǎn)環(huán)境,同時(shí)我們期望測(cè)試即文檔,因此測(cè)試的名稱寫很長(zhǎng)也沒(méi)有關(guān)系,重要的是能清晰的表達(dá)我們這個(gè)測(cè)試所覆蓋的用例是什么。
  • 一個(gè)測(cè)試只測(cè)一種case。
  • 單元測(cè)試通常需要覆蓋大量的case來(lái)保證我們的代碼在絕大多數(shù)場(chǎng)景下都是按期望工作的。因此要做到這一點(diǎn)可以參考下面兩大原則。這里就不詳細(xì)講解這兩個(gè)原則,具體內(nèi)容可以Google。
    • CORRECT原則
    • Right-BICEP原則
  • 單元測(cè)試有一個(gè)考核的標(biāo)準(zhǔn)就是測(cè)試覆蓋率,指的是我們的代碼有百分之多少被單元測(cè)試測(cè)到了。
    • 測(cè)試覆蓋率分幾種:行覆蓋率,分支覆蓋率,路徑覆蓋率,條件覆蓋率等。每種都可以單獨(dú)設(shè)置百分比。通常我們會(huì)看中行覆蓋率和分支覆蓋率。
    • 通常行業(yè)里面常設(shè)置測(cè)試覆蓋率在85%以上。
    • 為什么不是100%?因?yàn)椴皇撬写a都能被測(cè)到的,比如private的構(gòu)造函數(shù)是無(wú)法被測(cè)到的,這種就會(huì)降低覆蓋率。
  • 通常所有的自動(dòng)化測(cè)試都是開(kāi)發(fā)人員來(lái)寫,比如單元測(cè)試,集成測(cè)試等。

測(cè)試金字塔

說(shuō)到單元測(cè)試,就不得不提測(cè)試金字塔,如下圖,最底層是單元測(cè)試,最頂層是UI測(cè)試。(測(cè)試金字塔有好幾種,但道理都是相通的)

看左邊的箭頭,越往下越快,越往上越慢,它主要包括編寫越快,運(yùn)行越快,定位問(wèn)題越快等。

看右邊的箭頭,越往下成本越低,越往上成本越高,包括時(shí)間成本,金錢成本,人員成本,維護(hù)成本等。

測(cè)試金字塔

什么是mock?

我們?cè)谧鰡卧獪y(cè)試的時(shí)候,常常可能訪問(wèn)外部系統(tǒng)或者外部類,這些外部的不可控性會(huì)讓我們的單元測(cè)試成本變得很高。

常見(jiàn)的外部不可控性有:HTTP訪問(wèn),增刪文件,隨機(jī)性,時(shí)間相關(guān)性,接口類等。

于是開(kāi)發(fā)者便開(kāi)始探索更廉價(jià)的方式來(lái)寫單元測(cè)試,mock就是其中的解決方案。

mock 對(duì)象運(yùn)行在本地完全可控環(huán)境內(nèi),利用 mock 對(duì)象模擬被依賴的資源,使開(kāi)發(fā)者可以輕易的創(chuàng)建一個(gè)穩(wěn)定的測(cè)試環(huán)境。

mock是Test double理論中的一種,如果對(duì)test double理論感興趣,可以到這里了解更多,這里就不展開(kāi)說(shuō)了。

如何用mock?

還是以java為例,java的世界中常用的mock框架比如mockito。

下面是一個(gè)mock的例子。

    @Test
    void should_return_100_when_get_list_size() {
        List map = mock(List.class);
        //當(dāng)調(diào)用list.size()方法時(shí)候,返回100
        when(map.size()).thenReturn(100);
        Assert.assertEquals(100, map.size());
    }

單元測(cè)試是我們測(cè)試的最小單位,因此我們只測(cè)當(dāng)前這個(gè)public的方法中的實(shí)現(xiàn),而方法中調(diào)用第三方類的東西,我們都應(yīng)該mock掉。

這樣的好處有兩個(gè):

  1. 不會(huì)因?yàn)槠渌惖牟豢煽匦远鴮?dǎo)致這個(gè)測(cè)試方法變得難寫。
  2. 其他類的修改不會(huì)導(dǎo)致這個(gè)測(cè)試方法掛掉。所有的變化都被隔離出去了。

什么是TDD?

最后再升華一下,簡(jiǎn)單說(shuō)一說(shuō)TDD,TDD的全稱是Test driven development,即測(cè)試驅(qū)動(dòng)開(kāi)發(fā)。它是極限編程XP中的一個(gè)標(biāo)準(zhǔn)實(shí)踐。

TDD要求在編寫某個(gè)功能的代碼之前先編寫測(cè)試代碼,然后只編寫使測(cè)試通過(guò)的功能代碼,通過(guò)測(cè)試來(lái)推動(dòng)整個(gè)開(kāi)發(fā)的進(jìn)行。

這樣做有四大好處:

  1. TDD是一個(gè)很好的契機(jī),可以讓你在考慮解決方案之前先考慮問(wèn)題。
  2. 首先考慮測(cè)試會(huì)迫使你首先考慮與代碼的接口。先思考接口可以幫助你將接口與實(shí)現(xiàn)分開(kāi)。
  3. 簡(jiǎn)單設(shè)計(jì)。
  4. 幾乎100%的測(cè)試覆蓋率。

這里我就不詳細(xì)敘述TDD相關(guān)的話題了,因?yàn)門DD是一個(gè)比較大的話題,如果感興趣,下次專門開(kāi)一個(gè)新話題來(lái)聊TDD。

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

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

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