阿里是如何進行單元測試培訓的?
一、什么是單元測試?(10 min)
維基百科中是這樣描述的:在計算機編程中,單元測試又稱為模塊測試,是針對程序模塊來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數(shù)、過程等;對于面向?qū)ο缶幊?,最小單元就是方法,包括基類、抽象類、或者派生類中的方法?/span>單元測試和集成測試的區(qū)別
回到測試的本質(zhì)來看,測試工作就是模擬真實環(huán)境,在代碼正式上線前進行驗證的工作,即使沒有任何工具和方法,這項工作也能夠通過人工操作來手動完成。但這種方式顯然不符合軟件從業(yè)者的習慣,于是開始出現(xiàn)了各種各樣的自動化測試方法,框架和工具。單元測試和集成測試使用的測試框架和工具大部分是相同的,而社區(qū)中對集成測試的介紹不盡相同,導致很多看過不同文章的同學對這兩種測試的認知存在爭議。首先需要達成一致的是,無論是單元測試還是集成測試,它們都是自動化測試。為了更好地區(qū)分,我們可以這樣理解:和生產(chǎn)代碼以及單元測試代碼在同一個代碼倉庫中,由開發(fā)同學自己編寫的,對外部環(huán)境(數(shù)據(jù)庫、文件系統(tǒng)、外部系統(tǒng)、消息隊列等)有真實調(diào)用的測試就是集成測試。下表中也從各種角度來對比了單元測試、集成測試和系統(tǒng)級別測試(包括端到端測試、鏈路測試、自動化回歸測試、UI測試等)的區(qū)別。|
|
單元測試 | 集成測試 | 系統(tǒng)級別測試 |
| 編寫人員 | 開發(fā) | 開發(fā) | 開發(fā) / 測試 |
| 編寫場地 | 生產(chǎn)代碼倉庫內(nèi) | 生產(chǎn)代碼倉庫內(nèi) | 生產(chǎn)代碼倉庫內(nèi) / 生產(chǎn)代碼倉庫外 |
| 編寫時間 | 代碼發(fā)布前 | 代碼發(fā)布前 | 代碼發(fā)布前 / 代碼發(fā)布后 |
| 編寫成本 | 低 | 中 | 高 |
| 編寫難度 | 低 | 中 | 高 |
| 反饋速度 | 極快,秒級 | 較慢,分鐘級 | 慢,天級別 |
| 覆蓋面積 | 代碼行覆蓋60-80% 分支覆蓋40-60% | 功能級別覆蓋HappyPath | 核心保障鏈路 |
| 環(huán)境依賴 | 代碼級別,不依賴環(huán)境 | 依賴日?;虮镜丨h(huán)境 | 依賴預發(fā)或生產(chǎn)環(huán)境 |
| 外部依賴模擬 | 全部模擬 | 部分模擬 | 不模擬,完全使用真實環(huán)境 |
小互動(10 min)
上手題 - 用于查看目前大家的單測理解和技能水平
以下是一個簡單的服務代碼,請認真觀看后寫下你認為應該寫的單元測試。
@Service
public class UserService {
/** 定義依賴對象 */
/** 用戶DAO */
@Autowired
private UserDAO userDAO;
/**
* 查詢用戶
*
* @param companyId 公司標識
* @param startIndex 開始序號
* @param pageSize 分頁大小
* @return 用戶分頁數(shù)據(jù)
*/
public PageDataVO<UserVO> queryUser(Long companyId, Long startIndex, Integer pageSize) {
//入?yún)⑿r?/span>
if(ValidationUtil.validate(companyId)){
throw new InvalidRequestException(companyId, "Invalid company Id");
}
// 查詢用戶數(shù)據(jù)
// 查詢用戶數(shù)據(jù): 總共數(shù)量
Long totalSize = userDAO.countByCompany(companyId);
// 查詢接口數(shù)據(jù): 數(shù)據(jù)列表
List<UserVO> dataList = null;
if (NumberHelper.isPositive(totalSize)) {
dataList = userDAO.queryByCompany(companyId, startIndex, pageSize);
}
// 返回分頁數(shù)據(jù)
return new PageDataVO<>(totalSize, dataList);
}
}
隨機問題:
-
你覺得針對這段代碼,應該需要寫幾個單元測試?
-
你覺得這個單元測試的行覆蓋率理論值可以達到多少?我們一般需要達到多少?如果要達到你的目標,投入的工作量是多少?
-
單測的代碼量和原業(yè)務代碼量的比值應該是多少比較合適?
答案(僅供參考):
(MockitoJUnitRunner.class)
public class UserServiceTest {
/** 定義靜態(tài)常量 */
/** 資源路徑 */
private static final String RESOURCE_PATH = "testUserService/";
/** 模擬依賴對象 */
/** 用戶DAO */
private UserDAO userDAO;
/** 定義測試對象 */
/** 用戶服務 */
private UserService userService;
/**
* 測試: 查詢用戶-無數(shù)據(jù)
*/
public void testQueryUser_Succeed_NoData() {
// 模擬依賴方法
// 模擬依賴方法: userDAO.countByCompany
Long companyId = 123L;
Long startIndex = 90L;
Integer pageSize = 10;
Mockito.doReturn(0L).when(userDAO).countByCompany(companyId);
// 調(diào)用測試方法
String path = RESOURCE_PATH + "testQueryUserWithoutData/";
PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
String text = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");
Assert.assertEquals("分頁數(shù)據(jù)不一致", text, JSON.toJSONString(pageData));
// 驗證依賴方法
// 驗證依賴方法: userDAO.countByCompany
Mockito.verify(userDAO).countByCompany(companyId);
// 驗證依賴對象
Mockito.verifyNoMoreInteractions(userDAO);
}
/**
* 測試: 查詢用戶-有數(shù)據(jù)
*/
public void testQueryUser_Succeed_WithData() {
// 模擬依賴方法
String path = RESOURCE_PATH + "testQueryUserWithData/";
// 模擬依賴方法: userDAO.countByCompany
Long companyId = 123L;
Mockito.doReturn(91L).when(userDAO).countByCompany(companyId);
// 模擬依賴方法: userDAO.queryByCompany
Long startIndex = 90L;
Integer pageSize = 10;
String text = ResourceHelper.getResourceAsString(getClass(), path + "dataList.json");
List<UserVO> dataList = JSON.parseArray(text, UserVO.class);
Mockito.doReturn(dataList).when(userDAO).queryByCompany(companyId, startIndex, pageSize);
// 調(diào)用測試方法
PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
text = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");
Assert.assertEquals("分頁數(shù)據(jù)不一致", text, JSON.toJSONString(pageData));
// 驗證依賴方法
// 驗證依賴方法: userDAO.countByCompany
Mockito.verify(userDAO).countByCompany(companyId);
// 驗證依賴方法: userDAO.queryByCompany
Mockito.verify(userDAO).queryByCompany(companyId, startIndex, pageSize);
// 驗證依賴對象
Mockito.verifyNoMoreInteractions(userDAO);
}
public void testQueryUser_Fail_WithBadInput() {}
}
搶答題:
- 請問這個是單元測試嗎?
(MockitoJUnitRunner.class)
public class SpringDalTest{
DBClient dbClient;
public void testGetDate_success_getFromDB(){
String result = dbClient.getDate("Request");
Assert.equals(result,"ExpectedResults");
}
}
- 請問這個是單元測試嗎?
(MockitoJUnitRunner.class)
public class DemoControllerTest {
DemoTairClient tairClient;
DemoDBMapper dbMapper;
DemoController demoController;
public void testGetResult_succeed_getFromCache() throws Exception {
when(tairClient.getCache(anyString())).thenReturn("getCacheResponse");
when(dbMapper.queryData(anyString())).thenReturn("queryDataResponse");
String result = demoController.getResult("request");
Assert.assertEquals("getCacheResponse", result);
}
}
- 請問這個是單元測試嗎?
public class SanityTest{
InfoService infoService1;
InfoService infoService2;
public void testGetInfo_succeed_giveValidRequest(){
String result1 = infoService1.getInfo("Request");
String result2 = infoService2.getInfo("Request");
Assert.equals(result1,result2);
}
}
二、為什么要寫單元測試?(10 min)
反例: 現(xiàn)在,領(lǐng)導要響應集團提高代碼質(zhì)量的號召,需要提升單元測試的代碼覆蓋率。當然,我們不能讓領(lǐng)導失望,那就加班加點地補充單元測試用例,努力提高單元測試的代碼覆蓋率。至于單元測試用例的有效性,我們大抵是不用關(guān)心的,因為我們只是面向指標編程。1230 提升到 LV3,331 提升團隊成為卓越工程 LV4, FY23 KPI 完成了,豈不是___,___,____? 正例: 因為我是一名專業(yè)的計算機工程師。理論上單元測試帶來的好處有:
-
單測成本低,速度快。
-
單測是最佳的、自動化的、可執(zhí)行的文檔。
-
測試的要訣是:測試你最擔心出錯的部分,這樣你就能從測試工作中得到最大的利益,100%覆蓋率的單測會逐漸消磨開發(fā)人員對測試的耐心和好感。
-
單測驅(qū)動設計,提升代碼簡潔度,確保安全重構(gòu),代碼修改后,單測仍然能通過,能夠增強開發(fā)者的信心。
-
快速反饋,更快的發(fā)現(xiàn)問題。
- 定位缺陷比集成測試更快更準確,降低修復成本。
實際上通過 2 個公司內(nèi)部的例子來證明:
例子 1
菜鳥 GOC 提供的一個外部 BU 的例子,僅供參考(對照組數(shù)據(jù)不夠嚴謹,數(shù)據(jù)還待完善,結(jié)論進一步佐證中)
外BU:采銷供應鏈--網(wǎng)絡&權(quán)限&商家&采購&物流協(xié)同(后面簡稱伍道團隊)單測縮短了變更的開發(fā)和測試總時長 1、變更開發(fā)測試時長領(lǐng)先 從阿里大腦獲取到 FY22 5月-10月采購供應鏈-伍道團隊,和供應鏈BU變更開發(fā)測試時長對比趨勢的客觀數(shù)據(jù),如下圖所示。對比6個月平均數(shù)來看,伍道團隊變更開發(fā)測試時長平均領(lǐng)先供應鏈BU 3.1天。
2、交付質(zhì)量高
:變更折返修改代碼再部署次數(shù)更低
采購供應鏈的有變更發(fā)布的全應用和BU內(nèi)非采購供應鏈全應用,將變更平均從預發(fā)環(huán)境折返修改代碼重部署次數(shù)做對比——5-10月份采購供應鏈“變更平均從預發(fā)環(huán)境折返修改代碼重部署”次數(shù)為X次,同BU其他部門平均次數(shù)Y次,相比低40%。
從Aone中應用,針對5-10月供應鏈BU在預發(fā)環(huán)境有發(fā)布成功的變更的核心應用,我們將非采購供應鏈所有核心應用49個(Aone未觀察有UT覆蓋率),和采購供應鏈主版本應用中,單測行覆蓋率超60%的7個核心應用做參照對比。說明進行單測并沒有使得變更的開發(fā)和測試時長變長,反而因為提升了代碼內(nèi)建質(zhì)量,縮短了變更的開發(fā)和測試總時長。

例子 2
菜鳥&企業(yè)智能:企業(yè)智能單測減少了變更從預發(fā)環(huán)境平均折返修改代碼重部署次數(shù) 菜鳥整體(單測一般)、企業(yè)智能(單測好)、A&B團隊(單測差),對比7-10月變更從預發(fā)環(huán)境平均折返修改代碼重部署次數(shù)?!?企業(yè)智能返工次數(shù)明顯低于菜鳥整體,低于菜鳥整體35%,低于單測建設薄弱團隊整體45%。
7-10月,企業(yè)智能5個BU核心應用平均全量行覆蓋≥60% 以及 菜鳥其他團隊 5個 無單測建設的BU核心應用對比變更從預發(fā)環(huán)境平均折返修改代碼重部署次數(shù)。——變更平均從預發(fā)環(huán)境折返修改代碼重部署次數(shù)為X,低于5個無單測應用對照組的Y,說明經(jīng)過充分的單測的變更的內(nèi)建質(zhì)量更好,因而在預發(fā)環(huán)境折返修改代碼重部署次數(shù)比對照組低52%

三、怎么寫單元測試?(50 min)
基礎單測套餐:
JUnit4 -? https://github.com/junit-team/junit4/wiki
Mockito2/3 -? https://site.mockito.org/1. 定義對象階段
定義對象階段主要包括:定義被測對象、模擬依賴對象(類成員)、注入依賴對象(類成員)。
2. 模擬方法階段
模擬方法階段主要包括:模擬依賴對象(參數(shù)、返回值和異常)、模擬依賴方法。
3. 調(diào)用方法階段
調(diào)用方法階段主要包括:模擬依賴對象(參數(shù))、調(diào)用被測方法、驗證參數(shù)對象(返回值和異常)。
4. 驗證方法階段
驗證方法階段主要包括:驗證依賴方法、驗證數(shù)據(jù)對象(參數(shù))、驗證依賴對象 。
大互動
看了這個理論知識,下面我們開始時間實操:
Before
需要 Maven 如何配置,要引入什么?
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
Test
(現(xiàn)場指導和實操環(huán)節(jié),附帶提問和一對一指導,內(nèi)容都在代碼里) 請下載: https://github.com/Lukegogogo/unit-test-training-demo/tree/mainline現(xiàn)場演示怎么寫單元測試,9 種通用場景。
- 【無依賴 難度:????】最簡單的 Helper/Util/Validation 層?
- 【有些許依賴 難度:??????】稍復雜 Service/Controller 層
- 【有很多依賴 難度:????????】更復雜的 Biz 邏輯層
- 【難度:????】如何測試 Exception
- 【難度:??】AssertJ 的使用
- 【難度:????】Verify 的使用
- 【難度:??????】Argument Captor 的使用
- 【難度:??】 靜態(tài)類的 mock
-
【難度:????????】依賴里面的 Lambda 表達式內(nèi)的邏輯怎么執(zhí)行?
四、單測開發(fā)規(guī)范 (15 min)
單測代碼規(guī)范要求
-
【強制】好的單元測試必須遵守AIR原則。說明:單元測試在線上運行時,感覺像空氣(AIR)一樣感覺不到,但在測試質(zhì)量的保障上,卻是非常關(guān)鍵的。好的單元測試宏觀上來說,具有自動化、獨立性、可重復執(zhí)行的特點。
- A :Automatic(自動化)
- I :Independent(獨立性)
- R :Repeatable(可重復)
-
【強制】單元測試應該是全自動執(zhí)行的,并且非交互式的。測試用例通常是被定期執(zhí)行的,執(zhí)行過程必須完全自動化才有意義。輸出結(jié)果需要人工檢查的測試不是一個好的單元測試。單元測試中不準使用System.out來進行人肉驗證,必須使用assert來驗證。
-
【強制】保持單元測試的獨立性。為了保證單元測試穩(wěn)定可靠且便于維護,單元測試用例之間決不能互相調(diào)用,也不能依賴執(zhí)行的先后次序。反例:method2需要依賴method1的執(zhí)行,將執(zhí)行結(jié)果做為method2的參數(shù)輸入。
-
【強制】單元測試是可以重復執(zhí)行的,不能受到外界環(huán)境的影響。說明:單元測試通常會被放到持續(xù)集成中,每次有代碼check in時單元測試都會被執(zhí)行。如果單測對外部環(huán)境(網(wǎng)絡、服務、中間件等)有依賴,容易導致持續(xù)集成機制的不可用。正例:為了不受外界環(huán)境影響,要求設計代碼時就把SUT的依賴改成注入,在測試時用spring 這樣的DI框架注入一個本地(內(nèi)存)實現(xiàn)或者Mock實現(xiàn)。
-
【強制】對于單元測試,要保證測試粒度足夠小,有助于精確定位問題。單測粒度至多是類級別,一般是方法級別。說明:只有測試粒度小才能在出錯時盡快定位到出錯位置。單測不負責檢查跨類或者跨系統(tǒng)的交互邏輯,那是集成測試的領(lǐng)域。
-
【強制】核心業(yè)務、核心應用、核心模塊的增量代碼確保單元測試通過。說明:新增代碼及時補充單元測試,如果新增代碼影響了原有單元測試,請及時修正。
-
【強制】單元測試代碼必須寫在如下工程目錄:src/test/java,不允許寫在業(yè)務代碼目錄下。說明:源碼編譯時會跳過此目錄,而單元測試框架默認是掃描此目錄。
-
【推薦】單元測試的基本目標:語句覆蓋率達到70%;核心模塊的語句覆蓋率和分支覆蓋率都要達到100%。說明:在工程規(guī)約>應用分層中提到的DAO層,Manager層,可重用度高的Service,都應該進行單元測試。
-
【推薦】編寫單元測試代碼遵守BCDE原則,以保證被測試模塊的交付質(zhì)量。
-
B :Border,邊界值測試,包括循環(huán)邊界、特殊取值、特殊時間點、數(shù)據(jù)順序等。
-
C :Correct,正確的輸入,并得到預期的結(jié)果。
-
D :Design,與設計文檔相結(jié)合,來編寫單元測試。
- E :Error,強制錯誤信息輸入(如:非法數(shù)據(jù)、異常流程、非業(yè)務允許輸入等),并得到預期的結(jié)果。
-
-
【推薦】對于數(shù)據(jù)庫相關(guān)的查詢,更新,刪除等操作,不能假設數(shù)據(jù)庫里的數(shù)據(jù)是存在的,或者直接操作數(shù)據(jù)庫把數(shù)據(jù)插入進去,請使用程序插入或者導入數(shù)據(jù)的方式來準備數(shù)據(jù)。反例:刪除某一行數(shù)據(jù)的單元測試,在數(shù)據(jù)庫中,先直接手動增加一行作為刪除目標,但是這一行新增數(shù)據(jù)并不符合業(yè)務插入規(guī)則,導致測試結(jié)果異常。
-
【推薦】和數(shù)據(jù)庫相關(guān)的單元測試,可以設定自動回滾機制,不給數(shù)據(jù)庫造成臟數(shù)據(jù)?;蛘邔卧獪y試產(chǎn)生的數(shù)據(jù)有明確的前后綴標識。正例:在企業(yè)智能事業(yè)部的內(nèi)部單元測試中,使用ENTERPRISE_INTELLIGENCE_UNIT_TEST_的前綴來標識單元測試相關(guān)代碼。
-
【推薦】對于不可測的代碼在適當時機做必要的重構(gòu),使代碼變得可測,避免為了達到測試要求而書寫不規(guī)范測試代碼。
-
【推薦】在設計評審階段,開發(fā)人員需要和測試人員一起確定單元測試范圍,單元測試最好覆蓋所有測試用例(UC)。
-
【推薦】單元測試作為一種質(zhì)量保障手段,在項目提測前完成單元測試,不建議項目發(fā)布后補充單元測試用例。
-
【參考】為了更方便地進行單元測試,業(yè)務代碼應避免以下情況:
- 構(gòu)造方法中做的事情過多。
- 存在過多的全局變量和靜態(tài)方法。
- 存在過多的外部依賴。
-
存在過多的條件語句。說明:多層條件語句建議使用衛(wèi)語句、策略模式、狀態(tài)模式等方式重構(gòu)。
-
【參考】不要對單元測試存在如下誤解:
-
那是測試同學干的事情。本文是開發(fā)規(guī)約,凡是本文出現(xiàn)的內(nèi)容都是與開發(fā)同學強相關(guān)的。
-
單元測試代碼是多余的。軟件系統(tǒng)的整體功能是否正常,與各單元部件的測試正常與否是強相關(guān)的。
-
單元測試代碼不需要維護。一年半載后,那么單元測試幾乎處于廢棄狀態(tài)。
- 單元測試與線上故障沒有辯證關(guān)系。好的單元測試能夠最大限度地規(guī)避線上故障。
-
研發(fā)流程規(guī)劃
-
技術(shù)方案編寫:?無論日需還是項目,無論改動大小 ,一定要進行技術(shù)方案編寫,按照技術(shù)方案模板對照本次改動是否涉及,如有則填寫詳情設計;如無,則表明不涉及;技術(shù)方案是你對需求的理解和分析,是對本次需求轉(zhuǎn)變成技術(shù)設計的思考過程,請盡量詳細編寫技術(shù)方案,進行必要的代碼設計,做到技術(shù)方案可直接coding的程度。在技術(shù)方案中,包含單測范圍和工時評估。
- 本地寫單測UT :?提交增量UT掃描任務,確保增量單測覆蓋率80%(不要僅數(shù)字,重點關(guān)注單測有效性和質(zhì)量),測試用例須全部通過。

- 提交CodeReview :日需和中小項目,在CR前必須完成步驟1&2,檢查入口:CR->質(zhì)量掃描
-
研發(fā)提測:
-
pipeline增加單測通過率和增量行覆蓋率展示
- Aone提測

五、有沒有什么神器?(5 min)
有一款好用的插件(TestMe),能夠自動生成單元測試代碼,且智能分析當前被測服務所需的依賴,并分析注入mock依賴,可以大大提高單元測試的效率。
裝好之后在你要測試的類里面按?+N,再選testme,就直接幫你生成好了
六、Q & A(10 min)
現(xiàn)場有同學在問:隨著單測覆蓋增加,單測性能怎么提升? 目前已有完善的方案分享給大家:?1.將配置升級至更好性能的機器(包括編譯升級一起)?
2.測試分組并發(fā)運行。? 兩者加起來,預計一般都可以降低到10分鐘內(nèi)。七、課后作業(yè)
-
分析團隊核心應用的核心鏈路,整理出單元測試作戰(zhàn)計劃
-
針對應用的核心鏈路:
-
業(yè)務邏輯層,寫一個類的單元測試,測試 case > 5, 覆蓋率達到 80%;
-
中間件層,寫一個類的單元測試,覆蓋率達到 90%;
- 針對自動生成的代碼,學會使用 exclude;
-
點個 ? 在看? 喜歡是一種感覺 在看是一種支持 ↘↘↘
