在今天的文章中打算和大家聊一聊關(guān)于測(cè)試的話題,也許有朋友會(huì)問,作為一名碼農(nóng)為什么要關(guān)注測(cè)試的問題?我們把代碼開發(fā)完基本自測(cè)沒問題了,扔給測(cè)試不就行了?有問題再改唄!也許有很多人都會(huì)這么想,的確,目前國(guó)內(nèi)很多程序員并不太關(guān)注Unit Test,很多互聯(lián)網(wǎng)公司也并沒有強(qiáng)制要求開發(fā)人員必須編寫Unit Test Case。究其原因,可能是國(guó)內(nèi)公司都比較有錢,測(cè)試團(tuán)隊(duì)動(dòng)輒幾十人,甚至上百人的公司大有人在。所以,從很多程序員的心態(tài)上看,測(cè)試這么多,直接扔給他們測(cè)試就好了!而另外一個(gè)被提及的原因,則是國(guó)內(nèi)互聯(lián)網(wǎng)公司產(chǎn)品迭代速度太快,需求太多做不過來,那里有時(shí)間寫Unit Test呢?
創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供華安網(wǎng)站建設(shè)、華安做網(wǎng)站、華安網(wǎng)站設(shè)計(jì)、華安網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計(jì)與制作、華安企業(yè)網(wǎng)站模板建站服務(wù),10年華安做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。
也許原因是多樣的,但拋開各種各樣的因素,今天我們只從程序員成長(zhǎng)的角度來聊一聊該不該寫Unit Test?最近這段時(shí)間和海外的程序員朋友合作開發(fā)項(xiàng)目比較多,給我的感受是他們特別強(qiáng)調(diào)Unit Test,用他們的話來說比較在意程序的品質(zhì)。而反觀國(guó)內(nèi)很多公司這一點(diǎn)做的就并不是那么好了!之前也和他們聊過其中的原因,他們認(rèn)為是國(guó)內(nèi)最近這些年的發(fā)展太快了,以至于有些過程被跳過了。
我們知道開發(fā)一個(gè)軟件或者平日里在現(xiàn)有的項(xiàng)目中開發(fā)某個(gè)需求時(shí),嚴(yán)格來說一般會(huì)經(jīng)歷這么一個(gè)流程,如下圖所示:
從圖上可以看到,在這個(gè)流程中軟件被交付集成測(cè)試之前,一定要先跑過Unit Test,而現(xiàn)在很多國(guó)內(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!
在前面的內(nèi)容中,我們講到Unit Test是一件非常有價(jià)值的事情,那么在實(shí)際的項(xiàng)目中Unit Test到底該怎么寫呢?以使用Spring Boot框架并基于Spring MVC開發(fā)的Web服務(wù)為例,大部分情況下的代碼結(jié)構(gòu)如圖所示:
在這個(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ù)庫(kù)就通過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ù)庫(kù)、有些需要訪問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ì)讓這件事變得非常容易!
在上面我們談到了在編寫業(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的一般步驟如下:
如果我們?cè)赟pring Boot的工程中引入了測(cè)試依賴Jar,實(shí)際上就已經(jīng)引入了Junit及Mockito這兩組測(cè)試框架的依賴。如下:
org.springframework.boot
spring-boot-starter-test
test
下面我們以一個(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ù)庫(kù)的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),如下圖所示:
一般來說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)秀,加油!