1 概述
在還沒有寫什么代碼之前,就引入單元測試,是要強調單元測試的重要性。當一套代碼的生命周期比較長的時候,單元測試更加重要。生命周期長的代碼,不管是產品人員還是開發人員,可能都會換了一批又一批,維護才是成本的大頭。后來的人缺乏這套代碼的歷史知識,改起來就會bug頻出,質量堪憂。如果有單元測試代碼,則每次的修改如果有問題,就能夠通過單元測試代碼暴露出來,這樣改代碼會比較讓人放心。但單元測試代碼是一項做了則太平無事看不出作用的事情,但不做則經常要滅火而追悔莫及的事情。如果公司不強制要求或者工期比較緊張的,很可能是被忽略的,等到代碼積累多了、歷史知識又丟失了不少的時候,再來想補,就困難重重了。
所以,在此要先引入單元測試的基礎設施,并在寫代碼的時候都加上單元測試,以達到編寫代碼達到100%的測試覆蓋率。這個“100%”如果變成什么“80%”之類的,效果就會差很多,因為哪些測了哪些沒測就分不清楚了,就很難監督單元測試的執行了。如果實在有些是測不了的,那么應該在工具中排除掉,排除掉的代碼應該盡量少,在此基礎上保持“100%”的覆蓋率,使得能夠比較好的監督單元測試有沒有在正常執行。
2 單元測試
2.1 引入依賴
Springboot已經對測試給出了自己的推薦,也是目前比較流行的選擇,如果沒有什么特殊需求的話,直接選用即可。
首先,在<dependencies>中引入test的依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>
就是這樣一個依賴,就基本足夠了。
2.2 具體依賴
找到 https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-test/2.7.18/spring-boot-starter-test-2.7.18.pom 文件,看看里面的具體依賴:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>2.7.18</version><scope>compile</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-test</artifactId><version>2.7.18</version><scope>compile</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-test-autoconfigure</artifactId><version>2.7.18</version><scope>compile</scope></dependency><dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><version>3.22.0</version><scope>compile</scope></dependency><dependency><groupId>org.hamcrest</groupId><artifactId>hamcrest</artifactId><version>2.2</version><scope>compile</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.8.2</version><scope>compile</scope></dependency><dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artifactId><version>4.5.1</version><scope>compile</scope></dependency><dependency><groupId>org.mockito</groupId><artifactId>mockito-junit-jupiter</artifactId><version>4.5.1</version><scope>compile</scope></dependency>
</dependencies>
上面是一些主要依賴的摘錄,前三個是引入springboot的基礎,單元測試庫junit已經是事實標準,通過junit-jupiter一起引入;斷言庫同時引入了assertj和hamcrest,它們各有各的優勢,這里優先使用assertj;mock庫則選用了mockito。mock對象,進行單元測試,最后斷言結果,這是測試的三個步驟,分別使用了對應的庫,這幾種庫的語法需要學一學。
單元測試的代碼要求邏輯簡單,就做上面三件事情,不要有什么業務邏輯,單元測試之間也不要有什么關系,保證單元測試代碼簡單,因為沒有人再來測試單元測試代碼了。
3 測試覆蓋率
單元測試是一個做了不好看到效果,如果沒有限制,那做多多少全憑開發的心情,那大概率很快就會荒廢掉。所以需要有監督方式,其中就是引入測試覆蓋率工具,這里選用JaCoCo,通過它來生成測試覆蓋率,每次寫完代碼都應該檢查測試覆蓋率,確保單元測試覆蓋所有預期的代碼。
3.1 工具引入
引入JaCoCo,只需要在pom.xml中的里增加插件,版本根據情況選比較新的:
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.13</version>
</plugin>
3.2 生成測試覆蓋率
3.2.1 IDEA生成測試覆蓋率
在IDEA中,選中單元測試用例類(可以整個目錄),然后右鍵選擇菜單“Run Tests in project_name with Coverage”:
IDEA會執行對應的測試用例,如果是在測試的根目錄執行,則可以得到所有測試用例的測試覆蓋率,并把結果展示到一個“Coverage”窗口里:
針對那些覆蓋率不到100%的類進行檢查和補充測試用例,如果要看詳情則可以導出報告:
導出的報告里有個index.html,打開該文件會展示各個類的測試覆蓋率,選擇覆蓋率不到100%的類,里面是綠色的則是覆蓋的,紅色的則是未覆蓋的,針對紅色部分的代碼進行補充測試用例。
3.2.2 命令行生成測試覆蓋率
IDEA會在底下默默做一些事情,讓操作比較簡單,這對開發人員比較友好。但如果想在持續集成工具如Jenkins上也能夠生成測試覆蓋率,那么還需要增加點配置:
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.13</version><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>test</phase><goals><goal>report</goal></goals><configuration><outputDirectory>${project.build.directory/}coverage_reports</outputDirectory></configuration></execution></executions>
</plugin>
上面<plugin>配置增加了一個<execution>,可以通過命令mvn clean test在執行測試用例的時候就把覆蓋率報告生成到coverage_reports目錄下,該目錄可以根據需要進行修改。
3.2.3 覆蓋率100%
作為開發人員,覆蓋率就應該追求覆蓋率100%。在制度上也應該這樣設定,這樣比較容易監督。實在無法測試或者沒有必要測試的則可以排除掉,排除的類也可以定期進行檢查,看看是否真的符合排除的條件。這個需要在工具上能夠檢測,使用mvn jacoco:check命令進行檢測:
<plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.13</version><configuration><excludes><exclude>com/qqian/stepfmk/srvpro/SrvproApplication.class</exclude></excludes><rules><rule><element>PACKAGE</element><limits><limit><counter>LINE</counter><value>COVEREDRATIO</value><minimum>1</minimum></limit></limits></rule></rules></configuration><executions><execution><id>prepare-agent</id><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>test</phase><goals><goal>report</goal></goals><configuration><outputDirectory>${project.build.directory/}coverage_reports</outputDirectory></configuration></execution><execution><id>check</id><phase>verify</phase><goals><goal>check</goal></goals></execution></executions>
</plugin>
上面配置中增加了check的<execution>節點,同時在這個插件的根里增加了配置:
1、排除掉一部分無法測試的類(可以用*、**來模糊匹配),如springboo的啟動類沒有必要進行測試;
2、增加<rules>節點指定按行(LINE)檢查覆蓋率,要達到100%。規則的配置可以參考:https://www.jacoco.org/jacoco/trunk/doc/check-mojo.html
3.2.4 黑盒測試覆蓋率
公司一般還有專門的測試部門,他們做的主要是黑盒測試。Jacoco也支持在java程序在運行的過程中收集測試覆蓋率信息,這樣就可以了解到黑盒測試到底覆蓋了哪些代碼,哪些沒有被覆蓋到。
在啟動Java程序的時候,增加一個agent服務器,用于后面dump測試覆蓋率的report:
java -jar srvpro-1.0.0-SNAPSHOT.jar -javaagent:jacocoagent.jar=excludes=com/qqian/stepfmk/srvpro/SrvproApplication.class,output=tcpserver,address=127.0.0.1,port=6300
-javaagent:jacocoagent.jar的格式為:-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2],即其value是一個key-value對形式,用逗號分隔,key和value用等號分隔。更詳細可參考:https://www.eclemma.org/jacoco/trunk/doc/agent.html
jacocoagent.jar包下載:https://www.eclemma.org/jacoco/index.html
執行測試后,需要用cli命令先dump出數據,然后用report命令把數據轉為hmtl文件才可以看:參考https://www.jacoco.org/jacoco/trunk/doc/cli.html
# dump數據
java -jar jacococli.jar dump [--address <address>] --destfile <path> [--help] [--port <port>] [--quiet] [--reset] [--retry <count>]
例子:java -jar jacococli.jar dump --address 127.0.0.1 --port 6300 --destfile /path/jacoco.exec# 轉成report
java -jar jacococli.jar report [<execfiles> ...] --classfiles <path> [--csv <file>] [--encoding <charset>] [--help] [--html <dir>] [--name <name>] [--quiet] [--sourcefiles <path>] [--tabwith <n>] [--xml <file>]
例子:java -jar jacococli.jar report /path/jacoco.exec
4 架構一小步
1、通過spring-boot-starter-test引入測試庫junit、斷言庫assertj、mock庫mockito;
2、規范:把單元測試放到比較重要的位置,測試覆蓋率要達100%;
3、規范:單元測試代碼只做mock對象、測試業務邏輯、斷言三件事,測試代碼要保持簡單,不能有業務邏輯。