作者 | 問(wèn)元
為什么需要單元測(cè)試
縱觀優(yōu)秀的開源工程,完備的單元測(cè)試總是必須的條件。通過(guò)這些單元測(cè)試,我們可以充分了解代碼中相關(guān)類和方法的作用和核心邏輯,熟悉各種場(chǎng)景的運(yùn)行情況。同時(shí)也因?yàn)橛辛藛卧獪y(cè)試,開源作者在接受各種feature的代碼提交時(shí)才有穩(wěn)定安全的保障。
其實(shí)單元測(cè)試的重要性所有開發(fā)同學(xué)應(yīng)該都了然于胸,同樣TDD(測(cè)試驅(qū)動(dòng)開發(fā))也不是一個(gè)新的概念,但是真當(dāng)我們落地實(shí)踐時(shí),又總會(huì)找出各種各樣的理由來(lái)勸服自己下次一定好好寫單元測(cè)試,這一次先放過(guò)自己。這些理由無(wú)外乎,開發(fā)周期太緊了; 測(cè)試同學(xué)能保證功能正確性;寫單元測(cè)試代碼量比業(yè)務(wù)代碼還大; 又不是不能跑。所以雖然我們總是在追逐工程師文化,卻又時(shí)不時(shí)放縱在放棄工程師底蘊(yùn)的路上。
單元測(cè)試是工程交付前質(zhì)量保障的第一環(huán),也無(wú)疑是軟件工程質(zhì)量保障的重要基石,有效的單元測(cè)試能夠提前發(fā)現(xiàn)90%以上的代碼Bug問(wèn)題,同時(shí)也能防止代碼的腐化,在工程重構(gòu)演進(jìn)時(shí)起到至關(guān)重要的作用。
怎么寫單元測(cè)試
好的單元測(cè)試的幾個(gè)要點(diǎn)
摘自阿里巴巴開發(fā)規(guī)約:
- 單元測(cè)試必須遵守AIR原則,單元測(cè)試必須具備Automatic(自動(dòng)化),Independent(獨(dú)立性),Repeatable(可重復(fù))性;
- 單元測(cè)試應(yīng)該是全自動(dòng)執(zhí)行的,并且非交互式的。測(cè)試用例通常是被定期執(zhí)行的,執(zhí)行過(guò)程必須完全自動(dòng)化才有意義。輸出結(jié)果需要人工檢查的測(cè)試不是一個(gè)好的單元測(cè)試;
- 單元測(cè)試要保證測(cè)試粒度足夠小。單元測(cè)試測(cè)試粒度足夠小,有助于精確定位問(wèn)題。單測(cè)粒度至多是類級(jí)別,一般是方法級(jí)別;
- 單元測(cè)試要遵守BCDE原則,Border,邊界值測(cè)試,包括循環(huán)邊界、特殊取值、特殊時(shí)間點(diǎn)、數(shù)據(jù)順序等;Correct,正確的輸入,并得到預(yù)期的結(jié)果;Design,與設(shè)計(jì)文檔相結(jié)合,來(lái)編寫單元測(cè)試;Error,強(qiáng)制錯(cuò)誤信息輸入(如:非法數(shù)據(jù)、異常流程、非業(yè)務(wù)允許輸入等),并得到預(yù)期的結(jié)果;
- 核心業(yè)務(wù)、核心應(yīng)用、核心模塊的增量代碼要確保單元測(cè)試通過(guò);
單元測(cè)試編碼范式
這里主要以Mockito單元測(cè)試框架為模版
1.Mock :通過(guò)when().thenReturn/thenAnswer/thenThrow 或者doReturn().when()等mock方式將依賴類方法進(jìn)行模擬,模擬服務(wù)依賴或者中間結(jié)果。
2.DO : 調(diào)用被測(cè)試類方法,執(zhí)行測(cè)試鏈路。
3.Verify : 校驗(yàn)執(zhí)行結(jié)果正確性,通過(guò)Assert校驗(yàn)數(shù)據(jù)結(jié)果準(zhǔn)確,通過(guò)Verify校驗(yàn)鏈路執(zhí)行準(zhǔn)確,通過(guò)expected=Exception.class校驗(yàn)異常鏈路。
public class Test {
// 0. 依賴類
@Mock
DependencyClass dependencyClass;
// 0. 待測(cè)試類
@InjectMocks
TestClass testClass;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testMethod() {
// 1. Mock, 依賴方法,構(gòu)造中間層數(shù)據(jù)
when(dependencyClass.someMehod(any())).thenReturn(mockData());
// 2. Do, 調(diào)用被測(cè)試類
Result result = testClass.testMehod();
// 3. Verify, 校驗(yàn)結(jié)果數(shù)據(jù)
Assert.assertEquals("some expected result string", result.getModel());
}
}
當(dāng)然寫單元測(cè)試用例雖然套路比較模版化,但是我們也要充分利用單元測(cè)試框架(Junit/Mockito/PowerMock/Spock),掌握其中的一些技巧,才能寫出快準(zhǔn)狠的單元測(cè)試用例,這也是研發(fā)同學(xué)必須要掌握的基本功。
單元測(cè)試編碼提效
IDEA上有很多單元測(cè)試插件,能夠半自動(dòng)化生成單元測(cè)試類文件,這里重點(diǎn)推薦TestMe插件。TestMe插件可以智能分析被測(cè)試類的依賴類,結(jié)合Mockito+Junit等單元測(cè)試框架,生成Mock/InjectMocks依賴關(guān)系,自動(dòng)生成單元測(cè)試類。
假設(shè)業(yè)務(wù)代碼如下:
@Component
public class DefaultMemberManager implements MemberManager {
@Autowired
private MemberDAO memberDAO;
@Autowired
private CacheManager cacheManager;
@Override
public Date queryActivationTime(long userId) {
Date activationTime = cacheManager.getActivationTime(userId);
if (activationTime == null) {
MemberDO memberDO = memberDAO.queryByUserId(userId);
if (memberDO != null) {
cacheManager.saveActivationTime(userId, memberDO.getActiveTime());
activationTime = memberDO.getActiveTime();
}
}
return activationTime;
}
}
則通過(guò)TestMe快捷鍵COMMOND+N, 可以極速自動(dòng)生成如下的單元測(cè)試類
public class DefaultMemberManagerTest {
@Mock
MemberDAO memberDAO;
@Mock
CacheManager cacheManager;
@InjectMocks
DefaultMemberManager defaultMemberManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testQueryActivationTime() throws Exception {
when(memberDAO.queryByUserId(anyLong())).thenReturn(null);
when(cacheManager.getActivationTime(anyLong())).thenReturn(
new GregorianCalendar(2022, Calendar.MARCH, 5, 23, 2).getTime());
Date result = defaultMemberManager.queryActivationTime(0L);
Assert.assertEquals(new GregorianCalendar(2022, Calendar.MARCH, 5, 23, 2).getTime(), result);
}
}
團(tuán)隊(duì)單元測(cè)試建設(shè)
覆蓋率概念
覆蓋率是類JaCoCo插件通過(guò)javaagent掛載的方式,在單元測(cè)試命令運(yùn)行時(shí)執(zhí)行代碼覆蓋率檢測(cè),計(jì)算單元測(cè)試執(zhí)行過(guò)程中所覆蓋的代碼比例來(lái)生成覆蓋率。常見的覆蓋率指標(biāo),又可進(jìn)一步細(xì)分為語(yǔ)句覆蓋率,條件覆蓋率,分支覆蓋率,路徑覆蓋率等。這里我們當(dāng)前更為關(guān)注語(yǔ)句覆蓋率和分支覆蓋率,尤其是增量代碼的覆蓋率,更能體現(xiàn)變更代碼的單元測(cè)試覆蓋情況。
如何進(jìn)行單元測(cè)試
這里我們借助于阿里研發(fā)平臺(tái)Aone的測(cè)試實(shí)驗(yàn)室功能,Aone實(shí)驗(yàn)室支持測(cè)試任務(wù)插件的編排組合,通過(guò)獨(dú)立的測(cè)試資源執(zhí)行測(cè)試任務(wù)。所以我們將代碼拉取插件,單元測(cè)試插件和覆蓋率計(jì)算插件進(jìn)行編排配置,形成最終的執(zhí)行流:拉取代碼;執(zhí)行單元測(cè)試命令;單元測(cè)試結(jié)果解析;計(jì)算覆蓋率。最終完成整個(gè)工程的單元測(cè)試覆蓋率計(jì)算。
單元測(cè)試覆蓋率結(jié)果示例如下:
什么時(shí)候觸發(fā)單元測(cè)試
單元測(cè)試任務(wù)主要通過(guò)持續(xù)交付流水線pipeline來(lái)集成,當(dāng)前幾個(gè)主要觸發(fā)策略如下
- 代碼提交時(shí),保證單元測(cè)試執(zhí)行及時(shí)性
- 代碼審核時(shí),保證代碼審核通過(guò)的代碼分支符合單元測(cè)試標(biāo)準(zhǔn)
- 發(fā)布流程中,保證最終集成發(fā)布的所有分支代碼符合單元測(cè)試標(biāo)準(zhǔn)
單元測(cè)試覆蓋率卡點(diǎn)
用戶平臺(tái)技術(shù)團(tuán)隊(duì)單元測(cè)試規(guī)范如下:
- 單元測(cè)試用例通過(guò)率為100%
- 單元測(cè)試增量代碼行覆蓋率為85%
- 代碼規(guī)范掃描增量問(wèn)題總數(shù)為0個(gè)
單元測(cè)試覆蓋率報(bào)表
為了更好的衡量單元測(cè)試的覆蓋率情況,我們采用報(bào)表的形式統(tǒng)計(jì)每個(gè)應(yīng)用,每個(gè)團(tuán)隊(duì)的代碼單元測(cè)試覆蓋率。
總結(jié)
當(dāng)前團(tuán)隊(duì)內(nèi)各應(yīng)用(除邊緣應(yīng)用外)的單元測(cè)試增量代碼覆蓋率在2022年已經(jīng)全部達(dá)到85%標(biāo)準(zhǔn),最新平均增量代碼行覆蓋率達(dá)到88%,整體全量代碼覆蓋率平均提高20%。誠(chéng)然單元測(cè)試覆蓋率的提高不是最終的目的,覆蓋率高不能完全代表工程質(zhì)量高,但是一個(gè)沒(méi)有單元測(cè)試或者單元測(cè)試覆蓋率低的工程,其代碼質(zhì)量和穩(wěn)定性必然不高。同時(shí)團(tuán)隊(duì)內(nèi)研發(fā)同學(xué)對(duì)單元測(cè)試也有了新的認(rèn)識(shí),自測(cè)和提測(cè)質(zhì)量顯著提升,全年未發(fā)生由于代碼質(zhì)量產(chǎn)生的線上故障,有效提升了工程質(zhì)量和服務(wù)穩(wěn)定性。
后續(xù)規(guī)劃,持續(xù)優(yōu)化單元測(cè)試質(zhì)量,提升分支覆蓋率,優(yōu)化邊界異常覆蓋;關(guān)注單元測(cè)試編碼效率的提升,優(yōu)化測(cè)試用例和測(cè)試數(shù)據(jù)分離;關(guān)注核心鏈路單元測(cè)試覆蓋率;熟練將TDD思維運(yùn)用到業(yè)務(wù)開發(fā)過(guò)程中。