11.28 聽說優秀的程序員20%的時間都在寫UT?

在今天的文章中打算和大家聊一聊關於測試的話題,也許有朋友會問,作為一名碼農為什麼要關注測試的問題?我們把代碼開發完基本自測沒問題了,扔給測試不就行了?有問題再改唄!也許有很多人都會這麼想,的確,目前國內很多程序員並不太關注Unit Test,很多互聯網公司也並沒有強制要求開發人員必須編寫Unit Test Case。究其原因,可能是國內公司都比較有錢,測試團隊動輒幾十人,甚至上百人的公司大有人在。所以,從很多程序員的心態上看,測試這麼多,直接扔給他們測試就好了!而另外一個被提及的原因,則是國內互聯網公司產品迭代速度太快,需求太多做不過來,那裡有時間寫Unit Test呢?

也許原因是多樣的,但拋開各種各樣的因素,今天我們只從程序員成長的角度來聊一聊該不該寫Unit Test?最近這段時間和海外的程序員朋友合作開發項目比較多,給我的感受是他們特別強調Unit Test,用他們的話來說比較在意程序的品質。而反觀國內很多公司這一點做的就並不是那麼好了!之前也和他們聊過其中的原因,他們認為是國內最近這些年的發展太快了,以至於有些過程被跳過了。

我們知道開發一個軟件或者平日裡在現有的項目中開發某個需求時,嚴格來說一般會經歷這麼一個流程,如下圖所示:

聽說優秀的程序員20%的時間都在寫UT?

從圖上可以看到,在這個流程中軟件被交付集成測試之前,一定要先跑過Unit Test,而現在很多國內公司的測試流程都繞過Unit Test直接過度到集成測試和QA測試,而從客觀的情況看,其實往往開發對邏輯是最瞭解的,如果開發可以通過覆蓋相對完整的Unit Test的話,實際上後續測試流程就會順利的多,而且寫Unit Test還有一個好處,就是能夠促使開發人員不斷優化代碼的設計邏輯,因為一旦你發現代碼無法被Unit Test的時候,就說明你的代碼不夠組件化而需要被重構了!作為一名程序員,如果你能夠在這種過程中不斷地審視自己寫過的代碼,相信你的代碼編寫水平一定會得到不斷地提高!

而從軟件可維護性的角度看,Unit Test覆蓋全面的項目往往都會比較好維護,因為完整的Unit Test實際上已經固化了軟件當前的邏輯,一旦有人在後續的開發中破壞了這個邏輯,就會導致Unit Test無法通過,此時如果要求無法被Unit Test跑過的代碼不能被編譯成功或者提交的話,那麼就會強迫修改者去完善Unit Test。這樣也從側面提高了程序員的測試意識,減少了發生重大Bug的幾率!

從以上兩個角度看,Unit Test一方面可以提高程序員的編碼水平,另外一方面也能儘量保證軟件的質量,所以Unit Test是一件非常有價值的事情,難怪他們說優秀的程序員20%的時間都在寫Unit Test!

Unit Test該怎麼寫

在前面的內容中,我們講到Unit Test是一件非常有價值的事情,那麼在實際的項目中Unit Test到底該怎麼寫呢?以使用Spring Boot框架並基於Spring MVC開發的Web服務為例,大部分情況下的代碼結構如圖所示:

聽說優秀的程序員20%的時間都在寫UT?

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

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

不過說到這裡大家可能會有很大的疑問,那就是我們在進行Unit Test時,正如上圖所示Service層本身依賴了很多其他組件,有些需要調用數據庫、有些需要訪問Redis、有些還需要調用第三方接口,在這種情況下好像很難讓Unit Test跑下去,因為不可能每次運行Unit Test的時候這些環境都是在線的,怎麼辦呢?所以在早期寫Unit Test,如果有第三方依賴無法被測試的情況下是需要我們手動編寫Mock測試代碼的,舉個例子假設我們有個業務層的類class A{...}需要被Unit Test,但是A中依賴於第三方組件代碼B,由於B需要連接外部網絡,所以我們在測試A的時候沒有辦法直接依賴B的實例,所以我們一般來說需要單獨定義個class MockB extend B{@Override ...},這個類繼承B並以Mock的方式重寫其方法,從而來為A類的Unit Test提供Mock Bean!而這種由於組件依賴複雜的情況,也在某種程度上限制來大家寫Unit Test的熱情,不過下面要介紹的這個神器會讓這件事變得非常容易!

Unit Test神器之Mockito

在上面我們談到了在編寫業務層Unit Test時候會發現複雜的組件依賴需要我們編寫很多額外的Mock類,增加來我們編寫Unit Test的難度,而Mockito這個測試框架的出現則讓Mock這件事變得非常容易了!Mockito是一個模擬測試框架,可以讓我們以註解(@MockBean)的方式優雅地進行依賴組件的Mock並對執行邏輯進行驗證。使用Mockito的一般步驟如下:

聽說優秀的程序員20%的時間都在寫UT?

  1. 模擬任何外部第三方組件依賴,並將這些模擬對象插入測試代碼;
  2. 執行測試中的代碼;
  3. 驗證代碼是否按照預期執行;

如果我們在Spring Boot的工程中引入了測試依賴Jar,實際上就已經引入了Junit及Mockito這兩組測試框架的依賴。如下:

<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-test/<artifactid>
<scope>test/<scope>
/<dependency>

下面我們以一個實際的案例來演示下如何編寫一個針對Service層代碼Unit Test,Service業務邏輯代碼如下:

@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("充值訂單重複");
}
//構建充值訂單

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);
//調用支付接口
paymentClient.consumeAccount(1, "1", "CNY");
//構建返回參數
AccountChargeTradeResVo accountChargeTradeResVo = AccountChargeTradeResVo.builder()
.userId(Long.valueOf(walletOrder.getUserId())).currency(walletOrder.getCurrency())
.orderId(walletOrder.getOrderId()).businessType(walletOrder.getBusiType()).build();
return accountChargeTradeResVo;
}
}

以上業務代碼實際上是演示了一個用戶錢包充值的大致邏輯的業務層方法,而該方法中有兩個依賴組件需要被Mock一個是表示操作數據庫的walletOrderDao,另外一個則是表示需要調用支付系統的客戶端依賴paymentClient。那麼使用Mockito該如何在Unit Test中進行Mock呢?

我們在工程對應的test目錄的包結構中,建立一個與業務層邏輯包結構一樣的測試代碼結構,如下圖所示:

聽說優秀的程序員20%的時間都在寫UT?

一般來說Unit Test類的代碼接口與實際源碼結構一致就行,以被測試類+Test後綴命名即可。接下來我們編寫該測試代碼:

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

在以上測試代碼中我們通過@MockBean這個註解就很容易的Mock了該業務層代碼的依賴組件,這樣測試代碼在執行依賴組件的邏輯時就會被Mock而不會真正調用這個方法。而一般情況下我們也可以驗證下Mock對象的方法是否有被調用,但是隻是驗證下調用本身是否觸發而並不是真的調用,可以使用given/verify這兩個Mocktio提供的方法來實現。

對於大部分情況採用這樣的模式進行Unit Test就差不多了,更多其他細節的用法大家可以在好好研究下Mocktio提供的功能!在這裡示例中還有個一個小的技巧,就是我們在使用@SpringBootTest的時候如:

@SpringBootTest(classes = {UserAccountTradeServiceImpl.class})

可以直接指定要測試的Service類,這樣Spring Boot就不會加載其他亂七八糟的依賴了,這樣會節約Unit Test運行的時間。

寫這篇文章最主要的目的還在於希望大家養成寫Unit Test的好習慣,做一個注重代碼品質的優秀程序員!希望大家都能夠越變越優秀,加油!


分享到:


相關文章: