溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

聽說優(yōu)秀的程序員20%的時(shí)間都在寫UT?

發(fā)布時(shí)間:2020-06-22 22:48:13 來源:網(wǎng)絡(luò) 閱讀:217 作者:wx5d9ed7c8443c3 欄目:編程語言

在今天的文章中打算和大家聊一聊關(guān)于測(cè)試的話題,也許有朋友會(huì)問,作為一名碼農(nóng)為什么要關(guān)注測(cè)試的問題?我們把代碼開發(fā)完基本自測(cè)沒問題了,扔給測(cè)試不就行了?有問題再改唄!也許有很多人都會(huì)這么想,的確,目前國內(nèi)很多程序員并不太關(guān)注Unit Test,很多互聯(lián)網(wǎng)公司也并沒有強(qiáng)制要求開發(fā)人員必須編寫Unit Test Case。究其原因,可能是國內(nèi)公司都比較有錢,測(cè)試團(tuán)隊(duì)動(dòng)輒幾十人,甚至上百人的公司大有人在。所以,從很多程序員的心態(tài)上看,測(cè)試這么多,直接扔給他們測(cè)試就好了!而另外一個(gè)被提及的原因,則是國內(nèi)互聯(lián)網(wǎng)公司產(chǎn)品迭代速度太快,需求太多做不過來,那里有時(shí)間寫Unit Test呢?

也許原因是多樣的,但拋開各種各樣的因素,今天我們只從程序員成長的角度來聊一聊該不該寫Unit Test?最近這段時(shí)間和海外的程序員朋友合作開發(fā)項(xiàng)目比較多,給我的感受是他們特別強(qiáng)調(diào)Unit Test,用他們的話來說比較在意程序的品質(zhì)。而反觀國內(nèi)很多公司這一點(diǎn)做的就并不是那么好了!之前也和他們聊過其中的原因,他們認(rèn)為是國內(nèi)最近這些年的發(fā)展太快了,以至于有些過程被跳過了。

我們知道開發(fā)一個(gè)軟件或者平日里在現(xiàn)有的項(xiàng)目中開發(fā)某個(gè)需求時(shí),嚴(yán)格來說一般會(huì)經(jīng)歷這么一個(gè)流程,如下圖所示:

聽說優(yōu)秀的程序員20%的時(shí)間都在寫UT?

從圖上可以看到,在這個(gè)流程中軟件被交付集成測(cè)試之前,一定要先跑過Unit Test,而現(xiàn)在很多國內(nèi)公司的測(cè)試流程都繞過Unit Test直接過度到集成測(cè)試和QA測(cè)試,而從客觀的情況看,其實(shí)往往開發(fā)對(duì)邏輯是最了解的,如果開發(fā)可以通過覆蓋相對(duì)完整的Unit Test的話,實(shí)際上后續(xù)測(cè)試流程就會(huì)順利的多,而且寫Unit Test還有一個(gè)好處,就是能夠促使開發(fā)人員不斷優(yōu)化代碼的設(shè)計(jì)邏輯,因?yàn)橐坏┠惆l(fā)現(xiàn)代碼無法被Unit Test的時(shí)候,就說明你的代碼不夠組件化而需要被重構(gòu)了!作為一名程序員,如果你能夠在這種過程中不斷地審視自己寫過的代碼,相信你的代碼編寫水平一定會(huì)得到不斷地提高!

而從軟件可維護(hù)性的角度看,Unit Test覆蓋全面的項(xiàng)目往往都會(huì)比較好維護(hù),因?yàn)橥暾腢nit Test實(shí)際上已經(jīng)固化了軟件當(dāng)前的邏輯,一旦有人在后續(xù)的開發(fā)中破壞了這個(gè)邏輯,就會(huì)導(dǎo)致Unit Test無法通過,此時(shí)如果要求無法被Unit Test跑過的代碼不能被編譯成功或者提交的話,那么就會(huì)強(qiáng)迫修改者去完善Unit Test。這樣也從側(cè)面提高了程序員的測(cè)試意識(shí),減少了發(fā)生重大Bug的幾率!

從以上兩個(gè)角度看,Unit Test一方面可以提高程序員的編碼水平,另外一方面也能盡量保證軟件的質(zhì)量,所以Unit Test是一件非常有價(jià)值的事情,難怪他們說優(yōu)秀的程序員20%的時(shí)間都在寫Unit Test!

Unit Test該怎么寫

在前面的內(nèi)容中,我們講到Unit Test是一件非常有價(jià)值的事情,那么在實(shí)際的項(xiàng)目中Unit Test到底該怎么寫呢?以使用Spring Boot框架并基于Spring MVC開發(fā)的Web服務(wù)為例,大部分情況下的代碼結(jié)構(gòu)如圖所示:

聽說優(yōu)秀的程序員20%的時(shí)間都在寫UT?

在這個(gè)軟件結(jié)構(gòu)中一般面向外部調(diào)用的是Controller層的服務(wù)接口定義,這一層由Spring MVC框架提供支持;而Controller層在接收到請(qǐng)求后需要將參數(shù)傳遞給Service層的業(yè)務(wù)方法進(jìn)行處理,而Service層的業(yè)務(wù)方法邏輯就會(huì)比較多樣,例如可能需要操作數(shù)據(jù)庫就通過Dao層提供的組件去實(shí)現(xiàn),也可能需要訪問個(gè)中間件組件之類,如緩存服務(wù)Redis、消息服務(wù)RocketMQ之類。除此之外,Service層邏輯可能還會(huì)涉及到其他第三方服務(wù)的調(diào)用,例如支付業(yè)務(wù)還需要調(diào)用支付寶之類的接口等等!

所以一般來說Unit Test的重點(diǎn)就是Service層的業(yè)務(wù)邏輯方法,如果Controller層也涉及到一些流程邏輯之類,也需要被Unit Test覆蓋一下!而具體的Unit Test用例編寫,遵循Maven工程規(guī)約即可。

不過說到這里大家可能會(huì)有很大的疑問,那就是我們?cè)谶M(jìn)行Unit Test時(shí),正如上圖所示Service層本身依賴了很多其他組件,有些需要調(diào)用數(shù)據(jù)庫、有些需要訪問Redis、有些還需要調(diào)用第三方接口,在這種情況下好像很難讓Unit Test跑下去,因?yàn)椴豢赡苊看芜\(yùn)行Unit Test的時(shí)候這些環(huán)境都是在線的,怎么辦呢?所以在早期寫Unit Test,如果有第三方依賴無法被測(cè)試的情況下是需要我們手動(dòng)編寫Mock測(cè)試代碼的,舉個(gè)例子假設(shè)我們有個(gè)業(yè)務(wù)層的類class A{...}需要被Unit Test,但是A中依賴于第三方組件代碼B,由于B需要連接外部網(wǎng)絡(luò),所以我們?cè)跍y(cè)試A的時(shí)候沒有辦法直接依賴B的實(shí)例,所以我們一般來說需要單獨(dú)定義個(gè)class MockB extend B{@Override ...},這個(gè)類繼承B并以Mock的方式重寫其方法,從而來為A類的Unit Test提供Mock Bean!而這種由于組件依賴復(fù)雜的情況,也在某種程度上限制來大家寫Unit Test的熱情,不過下面要介紹的這個(gè)神器會(huì)讓這件事變得非常容易!

Unit Test神器之Mockito

在上面我們談到了在編寫業(yè)務(wù)層Unit Test時(shí)候會(huì)發(fā)現(xiàn)復(fù)雜的組件依賴需要我們編寫很多額外的Mock類,增加來我們編寫Unit Test的難度,而Mockito這個(gè)測(cè)試框架的出現(xiàn)則讓Mock這件事變得非常容易了!Mockito是一個(gè)模擬測(cè)試框架,可以讓我們以注解(@MockBean)的方式優(yōu)雅地進(jìn)行依賴組件的Mock并對(duì)執(zhí)行邏輯進(jìn)行驗(yàn)證。使用Mockito的一般步驟如下:

聽說優(yōu)秀的程序員20%的時(shí)間都在寫UT?

  1. 模擬任何外部第三方組件依賴,并將這些模擬對(duì)象插入測(cè)試代碼;
  2. 執(zhí)行測(cè)試中的代碼;
  3. 驗(yàn)證代碼是否按照預(yù)期執(zhí)行;

如果我們?cè)赟pring Boot的工程中引入了測(cè)試依賴Jar,實(shí)際上就已經(jīng)引入了Junit及Mockito這兩組測(cè)試框架的依賴。如下:

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
</dependency>

下面我們以一個(gè)實(shí)際的案例來演示下如何編寫一個(gè)針對(duì)Service層代碼Unit Test,Service業(yè)務(wù)邏輯代碼如下:

@Service
public class UserAccountTradeServiceImpl implements UserAccountTradeService {

    @Autowired
    WalletOrderDao walletOrderDao;

    @Autowired
    PaymentClient paymentClient;

    @Override
    public AccountChargeTradeResVo accountChargeTrade(AccountChargeTradeReqVo accountChargeTradeReqVo)
            throws Exception {
        //充值交易防重
        WalletOrder walletOrder = walletOrderDao.selectOrderById(accountChargeTradeReqVo.getOrderId());
        if (walletOrder != null) {
            throw new Exception("充值訂單重復(fù)");
        }
        //構(gòu)建充值訂單
        walletOrder = WalletOrder.builder().orderId(accountChargeTradeReqVo.getOrderId())
                .userId(String.valueOf(accountChargeTradeReqVo.getUserId()))
                .amount(accountChargeTradeReqVo.getAmount())
                .busiType("0").tradeType("charge").currency(accountChargeTradeReqVo.getCurrency()).status("1")
                .isRenew(accountChargeTradeReqVo.getReNew()).tradeTime(new Timestamp(new Date().getTime()))
                .updateTime(new Timestamp(new Date().getTime()))
                .build();
        walletOrderDao.insertOrder(walletOrder);
        //調(diào)用支付接口
        paymentClient.consumeAccount(1, "1", "CNY");
        //構(gòu)建返回參數(shù)
        AccountChargeTradeResVo accountChargeTradeResVo = AccountChargeTradeResVo.builder()
                .userId(Long.valueOf(walletOrder.getUserId())).currency(walletOrder.getCurrency())
                .orderId(walletOrder.getOrderId()).businessType(walletOrder.getBusiType()).build();
        return accountChargeTradeResVo;
    }
}

以上業(yè)務(wù)代碼實(shí)際上是演示了一個(gè)用戶錢包充值的大致邏輯的業(yè)務(wù)層方法,而該方法中有兩個(gè)依賴組件需要被Mock一個(gè)是表示操作數(shù)據(jù)庫的walletOrderDao,另外一個(gè)則是表示需要調(diào)用支付系統(tǒng)的客戶端依賴paymentClient。那么使用Mockito該如何在Unit Test中進(jìn)行Mock呢?

我們?cè)诠こ虒?duì)應(yīng)的test目錄的包結(jié)構(gòu)中,建立一個(gè)與業(yè)務(wù)層邏輯包結(jié)構(gòu)一樣的測(cè)試代碼結(jié)構(gòu),如下圖所示:

聽說優(yōu)秀的程序員20%的時(shí)間都在寫UT?

一般來說Unit Test類的代碼接口與實(shí)際源碼結(jié)構(gòu)一致就行,以被測(cè)試類+Test后綴命名即可。接下來我們編寫該測(cè)試代碼:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {UserAccountTradeServiceImpl.class})
//@ActiveProfiles({"test"})
public class UserAccountTradeServiceImplTest {

    @MockBean
    private WalletOrderDao walletOrderDao;

    @MockBean
    private PaymentClient paymentClient;

    @Autowired
    private UserAccountTradeServiceImpl userAccountTradeServiceImpl;

    @Test
    public void accountChargeTradeTest() throws Exception {
        AccountChargeTradeReqVo accountChargeTradeReqVo = AccountChargeTradeReqVo.builder().orderId("12345")
                .userId(1001).amount(1000).currency("CNY").tradeTime("2019070412102023").reNew("1").build();
        AccountChargeTradeResVo accountChargeTradeResVo = userAccountTradeServiceImpl
                .accountChargeTrade(accountChargeTradeReqVo);
        assertNotNull(accountChargeTradeResVo);
        assertEquals(accountChargeTradeResVo.getOrderId(), accountChargeTradeReqVo.getOrderId());
        given(paymentClient.consumeAccount(any(Long.class), any(String.class), any(String.class))).willReturn(null);
        verify(paymentClient).consumeAccount(any(Long.class), any(String.class), any(String.class));
    }
}

在以上測(cè)試代碼中我們通過@MockBean這個(gè)注解就很容易的Mock了該業(yè)務(wù)層代碼的依賴組件,這樣測(cè)試代碼在執(zhí)行依賴組件的邏輯時(shí)就會(huì)被Mock而不會(huì)真正調(diào)用這個(gè)方法。而一般情況下我們也可以驗(yàn)證下Mock對(duì)象的方法是否有被調(diào)用,但是只是驗(yàn)證下調(diào)用本身是否觸發(fā)而并不是真的調(diào)用,可以使用given/verify這兩個(gè)Mocktio提供的方法來實(shí)現(xiàn)。

對(duì)于大部分情況采用這樣的模式進(jìn)行Unit Test就差不多了,更多其他細(xì)節(jié)的用法大家可以在好好研究下Mocktio提供的功能!在這里示例中還有個(gè)一個(gè)小的技巧,就是我們?cè)谑褂聾SpringBootTest的時(shí)候如:

@SpringBootTest(classes = {UserAccountTradeServiceImpl.class})

可以直接指定要測(cè)試的Service類,這樣Spring Boot就不會(huì)加載其他亂七八糟的依賴了,這樣會(huì)節(jié)約Unit Test運(yùn)行的時(shí)間。

寫這篇文章最主要的目的還在于希望大家養(yǎng)成寫Unit Test的好習(xí)慣,做一個(gè)注重代碼品質(zhì)的優(yōu)秀程序員!希望大家都能夠越變?cè)絻?yōu)秀,加油!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI