深入學(xué)習(xí)Java單元測(cè)試(Junit+Mock+代碼覆蓋率)
前言
單元測(cè)試是編寫測(cè)試代碼,用來(lái)檢測(cè)特定的、明確的、細(xì)顆粒的功能。單元測(cè)試并不一定保證程序功能是正確的,更不保證整體業(yè)務(wù)是準(zhǔn)備的。
單元測(cè)試不僅僅用來(lái)保證當(dāng)前代碼的正確性,更重要的是用來(lái)保證代碼修復(fù)、改進(jìn)或重構(gòu)之后的正確性。
一般來(lái)說(shuō),單元測(cè)試任務(wù)包括
- 1.接口功能測(cè)試:用來(lái)保證接口功能的正確性。
- 2.局部數(shù)據(jù)結(jié)構(gòu)測(cè)試(不常用):用來(lái)保證接口中的數(shù)據(jù)結(jié)構(gòu)是正確的
- 1.比如變量有無(wú)初始值
- 2.變量是否溢出
- 3.邊界條件測(cè)試
- 1.變量沒(méi)有賦值(即為NULL)
- 2.變量是數(shù)值(或字符)
- 1.主要邊界:最小值,最大值,無(wú)窮大(對(duì)于DOUBLE等)
- 2.溢出邊界(期望異?;蚓芙^服務(wù)):最小值-1,最大值+1
- 3.臨近邊界:最小值+1,最大值-1
- 3.變量是字符串
- 1.引用“字符變量”的邊界
- 2.空字符串
- 3.對(duì)字符串長(zhǎng)度應(yīng)用“數(shù)值變量”的邊界
- 4.變量是集合
- 1.空集合
- 2.對(duì)集合的大小應(yīng)用“數(shù)值變量”的邊界
- 3.調(diào)整次序:升序、降序
- 5.變量有規(guī)律
- 1.比如對(duì)于Math.sqrt,給出n^2-1,和n^2+1的邊界
- 4.所有獨(dú)立執(zhí)行通路測(cè)試:保證每一條代碼,每個(gè)分支都經(jīng)過(guò)測(cè)試
- 1.代碼覆蓋率
- 1.語(yǔ)句覆蓋:保證每一個(gè)語(yǔ)句都執(zhí)行到了
- 2.判定覆蓋(分支覆蓋):保證每一個(gè)分支都執(zhí)行到
- 3.條件覆蓋:保證每一個(gè)條件都覆蓋到true和false(即if、while中的條件語(yǔ)句)
- 4.路徑覆蓋:保證每一個(gè)路徑都覆蓋到
- 2.相關(guān)軟件
- 1.Cobertura:語(yǔ)句覆蓋
- 2.Emma: Eclipse插件Eclemma
- 1.代碼覆蓋率
- 5.各條錯(cuò)誤處理通路測(cè)試:保證每一個(gè)異常都經(jīng)過(guò)測(cè)試
JUNIT
JUnit是Java單元測(cè)試框架,已經(jīng)在Eclipse中默認(rèn)安裝。目前主流的有JUnit3和JUnit4。JUnit3中,測(cè)試用例需要繼承TestCase類。JUnit4中,測(cè)試用例無(wú)需繼承TestCase類,只需要使用@Test等注解。
Junit3
先看一個(gè)Junit3的樣例
// 測(cè)試java.lang.Math // 必須繼承TestCase public class Junit3TestCase extends TestCase { public Junit3TestCase() { super(); } // 傳入測(cè)試用例名稱 public Junit3TestCase(String name) { super(name); } // 在每個(gè)Test運(yùn)行之前運(yùn)行 @Override protected void setUp() throws Exception { System.out.println("Set up"); } // 測(cè)試方法。 // 方法名稱必須以test開(kāi)頭,沒(méi)有參數(shù),無(wú)返回值,是公開(kāi)的,可以拋出異常 // 也即類似public void testXXX() throws Exception {} public void testMathPow() { System.out.println("Test Math.pow"); Assert.assertEquals(4.0, Math.pow(2.0, 2.0)); } public void testMathMin() { System.out.println("Test Math.min"); Assert.assertEquals(2.0, Math.min(2.0, 4.0)); } // 在每個(gè)Test運(yùn)行之后運(yùn)行 @Override protected void tearDown() throws Exception { System.out.println("Tear down"); } }
如果采用默認(rèn)的TestSuite,則測(cè)試方法必須是public void testXXX() [throws Exception] {}的形式,并且不能存在依賴關(guān)系,因?yàn)闇y(cè)試方法的調(diào)用順序是不可預(yù)知的。
上例執(zhí)行后,控制臺(tái)會(huì)輸出
Set up Test Math.pow Tear down Set up Test Math.min Tear down
從中,可以猜測(cè)到,對(duì)于每個(gè)測(cè)試方法,調(diào)用的形式是:
testCase.setUp(); testCase.testXXX(); testCase.tearDown();
運(yùn)行測(cè)試方法
在Eclipse中,可以直接在類名或測(cè)試方法上右擊,在彈出的右擊菜單中選擇Run As -> JUnit Test。
在Mvn中,可以直接通過(guò)mvn test命令運(yùn)行測(cè)試用例。
也可以通過(guò)Java方式調(diào)用,創(chuàng)建一個(gè)TestCase實(shí)例,然后重載runTest()方法,在其方法內(nèi)調(diào)用測(cè)試方法(可以多個(gè))。
TestCase test = new Junit3TestCase("mathPow") { // 重載 protected void runTest() throws Throwable { testMathPow(); }; }; test.run();
更加便捷地,可以在創(chuàng)建TestCase實(shí)例時(shí)直接傳入測(cè)試方法名稱,JUnit會(huì)自動(dòng)調(diào)用此測(cè)試方法,如
TestCase test = new Junit3TestCase("testMathPow"); test.run();
Junit TestSuite
TestSuite是測(cè)試用例套件,能夠運(yùn)行過(guò)個(gè)測(cè)試方法。如果不指定TestSuite,會(huì)創(chuàng)建一個(gè)默認(rèn)的TestSuite。默認(rèn)TestSuite會(huì)掃描當(dāng)前內(nèi)中的所有測(cè)試方法,然后運(yùn)行。
如果不想采用默認(rèn)的TestSuite,則可以自定義TestSuite。在TestCase中,可以通過(guò)靜態(tài)方法suite()返回自定義的suite。
import junit.framework.Assert; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite; public class Junit3TestCase extends TestCase { //... public static Test suite() { System.out.println("create suite"); TestSuite suite = new TestSuite(); suite.addTest(new Junit3TestCase("testMathPow")); return suite; } }
允許上述方法,控制臺(tái)輸出
寫道 create suite Set up Test Math.pow Tear down
并且只運(yùn)行了testMathPow測(cè)試方法,而沒(méi)有運(yùn)行testMathMin測(cè)試方法。通過(guò)顯式指定測(cè)試方法,可以控制測(cè)試執(zhí)行的順序。
也可以通過(guò)Java的方式創(chuàng)建TestSuite,然后調(diào)用TestCase,如
// 先創(chuàng)建TestSuite,再添加測(cè)試方法 TestSuite testSuite = new TestSuite(); testSuite.addTest(new Junit3TestCase("testMathPow")); // 或者 傳入Class,TestSuite會(huì)掃描其中的測(cè)試方法。 TestSuite testSuite = new TestSuite(Junit3TestCase.class,Junit3TestCase2.class,Junit3TestCase3.class); // 運(yùn)行testSuite TestResult testResult = new TestResult(); testSuite.run(testResult);
testResult中保存了很多測(cè)試數(shù)據(jù),包括運(yùn)行測(cè)試方法數(shù)目(runCount)等。
JUnit4
與JUnit3不同,JUnit4通過(guò)注解的方式來(lái)識(shí)別測(cè)試方法。目前支持的主要注解有:
- @BeforeClass 全局只會(huì)執(zhí)行一次,而且是第一個(gè)運(yùn)行
- @Before 在測(cè)試方法運(yùn)行之前運(yùn)行
- @Test 測(cè)試方法
- @After 在測(cè)試方法運(yùn)行之后允許
- @AfterClass 全局只會(huì)執(zhí)行一次,而且是最后一個(gè)運(yùn)行
- @Ignore 忽略此方法
下面舉一個(gè)樣例:
import org.junit.After; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; public class Junit4TestCase { @BeforeClass public static void setUpBeforeClass() { System.out.println("Set up before class"); } @Before public void setUp() throws Exception { System.out.println("Set up"); } @Test public void testMathPow() { System.out.println("Test Math.pow"); Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0); } @Test public void testMathMin() { System.out.println("Test Math.min"); Assert.assertEquals(2.0, Math.min(2.0, 4.0), 0.0); } // 期望此方法拋出NullPointerException異常 @Test(expected = NullPointerException.class) public void testException() { System.out.println("Test exception"); Object obj = null; obj.toString(); } // 忽略此測(cè)試方法 @Ignore @Test public void testMathMax() { Assert.fail("沒(méi)有實(shí)現(xiàn)"); } // 使用“假設(shè)”來(lái)忽略測(cè)試方法 @Test public void testAssume(){ System.out.println("Test assume"); // 當(dāng)假設(shè)失敗時(shí),則會(huì)停止運(yùn)行,但這并不會(huì)意味測(cè)試方法失敗。 Assume.assumeTrue(false); Assert.fail("沒(méi)有實(shí)現(xiàn)"); } @After public void tearDown() throws Exception { System.out.println("Tear down"); } @AfterClass public static void tearDownAfterClass() { System.out.println("Tear down After class"); } }
如果細(xì)心的話,會(huì)發(fā)現(xiàn)Junit3的package是junit.framework,而Junit4是org.junit。
執(zhí)行此用例后,控制臺(tái)會(huì)輸出
寫道 Set up before class Set up Test Math.pow Tear down Set up Test Math.min Tear down Set up Test exception Tear down Set up Test assume Tear down Tear down After class
可以看到,執(zhí)行次序是@BeforeClass -> @Before -> @Test -> @After -> @Before -> @Test -> @After -> @AfterClass。@Ignore會(huì)被忽略。
運(yùn)行測(cè)試方法
與Junit3類似,可以在Eclipse中運(yùn)行,也可以通過(guò)mvn test命令運(yùn)行。
Assert
Junit3和Junit4都提供了一個(gè)Assert類(雖然package不同,但是大致差不多)。Assert類中定義了很多靜態(tài)方法來(lái)進(jìn)行斷言。列表如下:
- assertTrue(String message, boolean condition) 要求condition == true
- assertFalse(String message, boolean condition) 要求condition == false
- fail(String message) 必然失敗,同樣要求代碼不可達(dá)
- assertEquals(String message, XXX expected,XXX actual) 要求expected.equals(actual)
- assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual)
- assertNotNull(String message, Object object) 要求object!=null
- assertNull(String message, Object object) 要求object==null
- assertSame(String message, Object expected, Object actual) 要求expected == actual
- assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual
- assertThat(String reason, T actual, Matcher matcher) 要求matcher.matches(actual) == true
Mock/Stub
Mock和Stub是兩種測(cè)試代碼功能的方法。Mock測(cè)重于對(duì)功能的模擬。Stub測(cè)重于對(duì)功能的測(cè)試重現(xiàn)。比如對(duì)于List接口,Mock會(huì)直接對(duì)List進(jìn)行模擬,而Stub會(huì)新建一個(gè)實(shí)現(xiàn)了List的TestList,在其中編寫測(cè)試的代碼。
強(qiáng)烈建議優(yōu)先選擇Mock方式,因?yàn)镸ock方式下,模擬代碼與測(cè)試代碼放在一起,易讀性好,而且擴(kuò)展性、靈活性都比Stub好。
比較流行的Mock有:
其中EasyMock和Mockito對(duì)于Java接口使用接口代理的方式來(lái)模擬,對(duì)于Java類使用繼承的方式來(lái)模擬(也即會(huì)創(chuàng)建一個(gè)新的Class類)。Mockito支持spy方式,可以對(duì)實(shí)例進(jìn)行模擬。但它們都不能對(duì)靜態(tài)方法和final類進(jìn)行模擬,powermock通過(guò)修改字節(jié)碼來(lái)支持了此功能。
EasyMock
EasyMock把測(cè)試過(guò)程分為三步:錄制、運(yùn)行測(cè)試代碼、驗(yàn)證期望。
錄制過(guò)程大概就是:期望method(params)執(zhí)行times次(默認(rèn)一次),返回result(可選),拋出exception異常(可選)。
驗(yàn)證期望過(guò)程將會(huì)檢查方法的調(diào)用次數(shù)。
一個(gè)簡(jiǎn)單的樣例是:
@Test public void testListInEasyMock() { List list = EasyMock.createMock(List.class); // 錄制過(guò)程 // 期望方法list.set(0,1)執(zhí)行2次,返回null,不拋出異常 expect1: EasyMock.expect(list.set(0, 1)).andReturn(null).times(2); // 期望方法list.set(0,1)執(zhí)行1次,返回null,不拋出異常 expect2: EasyMock.expect(list.set(0, 1)).andReturn(1); // 執(zhí)行測(cè)試代碼 EasyMock.replay(list); // 執(zhí)行l(wèi)ist.set(0,1),匹配expect1期望,會(huì)返回null Assert.assertNull(list.set(0, 1)); // 執(zhí)行l(wèi)ist.set(0,1),匹配expect1(因?yàn)閑xpect1期望執(zhí)行此方法2次),會(huì)返回null Assert.assertNull(list.set(0, 1)); // 執(zhí)行l(wèi)ist.set(0,1),匹配expect2,會(huì)返回1 Assert.assertEquals(1, list.set(0, 1)); // 驗(yàn)證期望 EasyMock.verify(list); }
EasyMock還支持嚴(yán)格的檢查,要求執(zhí)行的方法次序與期望的完全一致。
Mockito
Mockito是Google Code上的一個(gè)開(kāi)源項(xiàng)目,Api相對(duì)于EasyMock更好友好。與EasyMock不同的是,Mockito沒(méi)有錄制過(guò)程,只需要在“運(yùn)行測(cè)試代碼”之前對(duì)接口進(jìn)行Stub,也即設(shè)置方法的返回值或拋出的異常,然后直接運(yùn)行測(cè)試代碼,運(yùn)行期間調(diào)用Mock的方法,會(huì)返回預(yù)先設(shè)置的返回值或拋出異常,最后再對(duì)測(cè)試代碼進(jìn)行驗(yàn)證。
官方提供了很多樣例,基本上包括了所有功能,可以去看看。
這里從官方樣例中摘錄幾個(gè)典型的:
驗(yàn)證調(diào)用行為
import static org.mockito.Mockito.*; //創(chuàng)建Mock List mockedList = mock(List.class); //使用Mock對(duì)象 mockedList.add("one"); mockedList.clear(); //驗(yàn)證行為 verify(mockedList).add("one"); verify(mockedList).clear();
對(duì)Mock對(duì)象進(jìn)行Stub
//也可以Mock具體的類,而不僅僅是接口 LinkedList mockedList = mock(LinkedList.class); //Stub when(mockedList.get(0)).thenReturn("first"); // 設(shè)置返回值 when(mockedList.get(1)).thenThrow(new RuntimeException()); // 拋出異常 //第一個(gè)會(huì)打印 "first" System.out.println(mockedList.get(0)); //接下來(lái)會(huì)拋出runtime異常 System.out.println(mockedList.get(1)); //接下來(lái)會(huì)打印"null",這是因?yàn)闆](méi)有stub get(999) System.out.println(mockedList.get(999)); // 可以選擇性地驗(yàn)證行為,比如只關(guān)心是否調(diào)用過(guò)get(0),而不關(guān)心是否調(diào)用過(guò)get(1) verify(mockedList).get(0);
代碼覆蓋率
比較流行的工具是Emma和Jacoco,Ecliplse插件有eclemma。eclemma2.0之前采用的是Emma,之后采用的是Jacoco。這里主要介紹一下Jacoco。Eclmama由于是Eclipse插件,所以非常易用,就不多做介紹了。
Jacoco
Jacoco可以嵌入到Ant、Maven中,也可以使用Java Agent技術(shù)監(jiān)控任意Java程序,也可以使用Java Api來(lái)定制功能。
Jacoco會(huì)監(jiān)控JVM中的調(diào)用,生成監(jiān)控結(jié)果(默認(rèn)保存在jacoco.exec文件中),然后分析此結(jié)果,配合源代碼生成覆蓋率報(bào)告。
需要注意的是:監(jiān)控和分析這兩步,必須使用相同的Class文件,否則由于Class不同,而無(wú)法定位到具體的方法,導(dǎo)致覆蓋率均為0%。
Java Agent嵌入
首先,需要下載jacocoagent.jar文件,然后在Java程序啟動(dòng)參數(shù)后面加上 -javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2],具體的options可以在此頁(yè)面找到。默認(rèn)會(huì)在JVM關(guān)閉時(shí)(注意不能是kill -9),輸出監(jiān)控結(jié)果到j(luò)acoco.exec文件中,也可以通過(guò)socket來(lái)實(shí)時(shí)地輸出監(jiān)控報(bào)告(可以在Example代碼中找到簡(jiǎn)單實(shí)現(xiàn))。
Java Report
可以使用Ant、Mvn或Eclipse來(lái)分析jacoco.exec文件,也可以通過(guò)API來(lái)分析。
public void createReport() throws Exception { // 讀取監(jiān)控結(jié)果 final FileInputStream fis = new FileInputStream(new File("jacoco.exec")); final ExecutionDataReader executionDataReader = new ExecutionDataReader(fis); // 執(zhí)行數(shù)據(jù)信息 ExecutionDataStore executionDataStore = new ExecutionDataStore(); // 會(huì)話信息 SessionInfoStore sessionInfoStore = new SessionInfoStore(); executionDataReader.setExecutionDataVisitor(executionDataStore); executionDataReader.setSessionInfoVisitor(sessionInfoStore); while (executionDataReader.read()) { } fis.close(); // 分析結(jié)構(gòu) final CoverageBuilder coverageBuilder = new CoverageBuilder(); final Analyzer analyzer = new Analyzer(executionDataStore, coverageBuilder); // 傳入監(jiān)控時(shí)的Class文件目錄,注意必須與監(jiān)控時(shí)的一樣 File classesDirectory = new File("classes"); analyzer.analyzeAll(classesDirectory); IBundleCoverage bundleCoverage = coverageBuilder.getBundle("Title"); // 輸出報(bào)告 File reportDirectory = new File("report"); // 報(bào)告所在的目錄 final HTMLFormatter htmlFormatter = new HTMLFormatter(); // HTML格式 final IReportVisitor visitor = htmlFormatter.createVisitor(new FileMultiReportOutput(reportDirectory)); // 必須先調(diào)用visitInfo visitor.visitInfo(sessionInfoStore.getInfos(), executionDataStore.getContents()); File sourceDirectory = new File("src"); // 源代碼目錄 // 遍歷所有的源代碼 // 如果不執(zhí)行此過(guò)程,則在報(bào)告中只能看到方法名,但是無(wú)法查看具體的覆蓋(因?yàn)闆](méi)有源代碼頁(yè)面) visitor.visitBundle(bundleCoverage, new DirectorySourceFileLocator(sourceDirectory, "utf-8", 4)); // 執(zhí)行完畢 visitor.visitEnd(); }
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java Druid連接池與Apache的DBUtils使用教程
這篇文章主要介紹了Java Druid連接池與Apache的DBUtils使用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-12-12Spring源碼之循環(huán)依賴之三級(jí)緩存詳解
這篇文章主要為大家詳細(xì)介紹了Spring源碼之循環(huán)依賴之三級(jí)緩存,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02SpringBoot中使用@scheduled定時(shí)執(zhí)行任務(wù)的坑
本文主要介紹了SpringBoot中使用@scheduled定時(shí)執(zhí)行任務(wù)的坑,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05微服務(wù)分布式架構(gòu)實(shí)現(xiàn)日志鏈路跟蹤的方法
在現(xiàn)有的系統(tǒng)中,由于大量的其他用戶/其他線程的日志也一起輸出穿行其中導(dǎo)致很難篩選出指定請(qǐng)求的全部相關(guān)日志。那我們?nèi)绾蝸?lái)處理呢?帶著這個(gè)問(wèn)題一起通過(guò)本文學(xué)習(xí)下吧2021-08-08Spring mvc整合mybatis(crud+分頁(yè)插件)操作mysql
這篇文章主要介紹了Spring mvc整合mybatis(crud+分頁(yè)插件)操作mysql的步驟詳解,需要的朋友可以參考下2017-04-04詳解idea中web.xml默認(rèn)版本問(wèn)題解決
這篇文章主要介紹了詳解idea中web.xml默認(rèn)版本問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12Spring中使用事務(wù)嵌套時(shí)需要警惕的問(wèn)題分享
最近項(xiàng)目上有一個(gè)使用事務(wù)相對(duì)復(fù)雜的業(yè)務(wù)場(chǎng)景報(bào)錯(cuò)了。在絕大多數(shù)情況下,都是風(fēng)平浪靜,沒(méi)有問(wèn)題。其實(shí)內(nèi)在暗流涌動(dòng),在有些異常情況下就會(huì)報(bào)錯(cuò),這種偶然性的問(wèn)題很有可能就會(huì)在暴露到生產(chǎn)上造成事故,那究竟是怎么回事呢?本文就來(lái)簡(jiǎn)單講講2023-04-04