外部依賴太多,如何寫 Java 單元測試?
本文轉載自微信公眾號「碼農私房話」,作者Liew 。轉載本文請聯系碼農私房話公眾號。
事出有因
在日常的開發中,很多人習慣性地寫完需求代碼后,嗖的一聲用 Postman 模擬真實請求或寫幾個 JUnit 的單元測試跑功能點,只要沒有問題就上線了,但其實這存在很大風險,一方面無法驗證業務邏輯的不同分支,另外一方面需嚴重依賴中間件資源才能運行測試用例,占用大量資源。
秣馬厲兵
Mockito是一個非常優秀的模擬框架,可以使用它簡潔的API來編寫漂亮的測試代碼,它的測試代碼可讀性高同時會產生清晰的錯誤日志。
添加 maven 依賴
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-core</artifactId>
- <version>3.3.3</version>
- <scope>test</scope>
- </dependency>
注意:Mockito 3.X 版本使用了 JDK8 API,但功能與 2.X 版本并沒有太大的變化。
指定 MockitoJUnitRunner
- @RunWith(MockitoJUnitRunner.class)
- public class MockitoDemoTest {
- //注入依賴的資源對象
- @Mock
- private MockitoTestService mockitoTestService;
- @Before
- public void before(){
- MockitoAnnotations.initMocks(this);
- }
- }
從代碼中觀察到,使用 @Mock 注解標識哪些對象需要被 Mock,同時在執行測試用例前初始化 MockitoAnnotations.initMocks(this) 告訴框架使 Mock 相關注解生效。
驗證對象行為 Verify
- @Test
- public void testVerify(){
- //創建mock
- List mockedList = mock(List.class);
- mockedList.add("1");
- mockedList.clear();
- //驗證list調用過add的操作行為
- verify(mockedList).add("1");
- //驗證list調用過clear的操作行為
- verify(mockedList).clear();
- //使用內建anyInt()參數匹配器,并存根
- when(mockedList.get(anyInt())).thenReturn("element");
- System.out.println(mockedList.get(2)); //此處輸出為element
- verify(mockedList).get(anyInt());
- }
存根 stubbing
stubbing 完全是模擬一個外部依賴、用來提供測試時所需要的數據。
- @Test
- public void testStub(){
- //可以mock具體的類,而不僅僅是接口
- LinkedList mockedList = mock(LinkedList.class);
- //存根(stubbing)
- when(mockedList.get(0)).thenReturn("first");
- when(mockedList.get(1)).thenThrow(new RuntimeException());
- //下面會打印 "first"
- System.out.println(mockedList.get(0));
- //下面會拋出運行時異常
- System.out.println(mockedList.get(1));
- //下面會打印"null" 因為get(999)沒有存根(stub)
- System.out.println(mockedList.get(999));
- doThrow(new RuntimeException()).when(mockedList).clear();
- //下面會拋出 RuntimeException:
- mockedList.clear();
- }
- 存根(stub)可以覆蓋,測試方法可以覆蓋全局設置的通用存根。
- 一旦做了存根,無論這個方法被調用多少次,方法將總是返回存根的值。
存根的連續調用
- @Test
- public void testStub() {
- when(mock.someMethod("some arg"))
- .thenThrow(new RuntimeException())
- .thenReturn("foo");
- mock.someMethod("some arg"); //第一次調用:拋出運行時異常
- //第二次調用: 打印 "foo"
- System.out.println(mock.someMethod("some arg"));
- //任何連續調用: 還是打印 "foo" (最后的存根生效).
- System.out.println(mock.someMethod("some arg"));
- //可供選擇的連續存根的更短版本:
- when(mock.someMethod("some arg")).thenReturn("one", "two", "three");
- when(mock.someMethod(anyString())).thenAnswer(new Answer() {
- Object answer(InvocationOnMock invocation) {
- Object[] args = invocation.getArguments();
- Object mock = invocation.getMock();
- return "called with arguments: " + args;
- }
- });
- // "called with arguments: foo
- System.out.println(mock.someMethod("foo"));
- }
在做方法存根時,可以指定不同時機需要提供的測試數據,例如第一次調用返回 xxx,第二次調用時拋出異常等。
參數匹配器
- @Test
- public void testArugument{
- //使用內建anyInt()參數匹配器
- when(mockedList.get(anyInt())).thenReturn("element");
- System.out.println(mockedList.get(999)); //打印 "element"
- //同樣可以用參數匹配器做驗證
- verify(mockedList).get(anyInt());
- //注意:如果使用參數匹配器,所有的參數都必須通過匹配器提供。
- verify(mock)
- .someMethod(anyInt(), anyString(), eq("third argument"));
- //上面是正確的 - eq(0也是參數匹配器),而下面的是錯誤的
- verify(mock)
- .someMethod(anyInt(), anyString(), "third argument");
- }
驗證調用次數
- @Test
- public void testVerify{
- List<String> mockedList = new ArrayList();
- mockedList.add("once");
- mockedList.add("twice");
- mockedList.add("twice");
- mockedList.add("three times");
- mockedList.add("three times");
- mockedList.add("three times");
- //下面兩個驗證是等同的 - 默認使用times(1)
- verify(mockedList).add("once");
- verify(mockedList, times(1)).add("once");
- verify(mockedList, times(2)).add("twice");
- verify(mockedList, times(3)).add("three times");
- //使用using never()來驗證. never()相當于 times(0)
- verify(mockedList, never()).add("never happened");
- //使用 atLeast()/atMost()來驗證
- verify(mockedList, atLeastOnce()).add("three times");
- verify(mockedList, atLeast(2)).add("five times");
- verify(mockedList, atMost(5)).add("three times");
- }
驗證調用順序
- @Test
- public void testOrder()
- {
- // A. 單個Mock,方法必須以特定順序調用
- List singleMock = mock(List.class);
- //使用單個Mock
- singleMock.add("was added first");
- singleMock.add("was added second");
- //為singleMock創建 inOrder 檢驗器
- InOrder inOrder = inOrder(singleMock);
- //確保add方法第一次調用是用"was added first",然后是用"was added second"
- inOrder.verify(singleMock).add("was added first");
- inOrder.verify(singleMock).add("was added second");
- }
以上是 Mockito 框架常用的使用方式,但 Mockito 有一定的局限性, 它只能 Mock 類或者接口,對于靜態、私有及final方法的 Mock 則無能為力了。
而 PowerMock 正是彌補這塊的缺陷,它的實現原理如下:
- 當某個測試方法被注解 @PrepareForTest 標注后,在運行測試用例時會創建一個新的 MockClassLoader 實例并加載該測試用例使用到的類(系統類除外)。
- PowerMock 會根據你的 mock 要求,去修改寫在注解 @PrepareForTest 里的 class 文件內容(調用非系統的靜態、Final方法),若是包含調用系統的方法則修改調用系統方法的類的 class 文件內容達到滿足需求 。
但值得高興的是在 Mockito2.7.2 及更高版本添加了對 final 類及方法支持[1] 。
同樣, Mockito3.4.0 及更高版本支持對靜態方法的 Mock[2],雖然是處于孵化階段,但對于我們做單元測試而言是已經足夠了。
決勝之機
大多數項目使用了 Spring 或 Spring Boot 作為基礎框架,研發只需要關心業務邏輯即可。
在代碼例子中將使用 Junit5 的版本,因此要求 Spring boot版本必須是2.2.0版本或以上,采用 Mockito3.5.11 的版本作為 Mock 框架,減少項目對 PowerMock 的依賴,另外還有一個重要原因是因為目前PowerMock不支持 Junit5,無法在引入 PowerMock 后使用Junit5 的相關功能及API,本文項目代碼地址:https://github.com/GoQeng/spring-mockito3-demo。
maven 配置
- <properties>
- <java.version>1.8</java.version>
- <mockito.version>3.5.11</mockito.version>
- <byte-buddy.version>1.10.15</byte-buddy.version>
- <redisson-spring.version>3.13.4</redisson-spring.version>
- <mysql.version>5.1.48</mysql.version>
- <jacoco.version>0.8.6</jacoco.version>
- <junit-jupiter.version>5.6.2</junit-jupiter.version>
- <junit-platform.version>1.1.1</junit-platform.version>
- <mybatis-spring.version>2.1.3</mybatis-spring.version>
- <maven-compiler.version>3.8.1</maven-compiler.version>
- <maven-surefire.version>2.12.4</maven-surefire.version>
- <h2.version>1.4.197</h2.version>
- </properties>
- <dependencies>
- <!-- spring boot相關依賴 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- <exclusions>
- <exclusion>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-core</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.junit.vintage</groupId>
- <artifactId>junit-vintage-engine</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <!-- Mockito -->
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-core</artifactId>
- <version>${mockito.version}</version>
- <scope>compile</scope>
- <exclusions>
- <exclusion>
- <groupId>net.bytebuddy</groupId>
- <artifactId>byte-buddy</artifactId>
- </exclusion>
- <exclusion>
- <groupId>net.bytebuddy</groupId>
- <artifactId>byte-buddy-agent</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <!-- 由于mockito-core自帶的byte-buddy版本低,無法使用mock靜態方法 -->
- <dependency>
- <groupId>net.bytebuddy</groupId>
- <artifactId>byte-buddy</artifactId>
- <version>${byte-buddy.version}</version>
- </dependency>
- <dependency>
- <groupId>net.bytebuddy</groupId>
- <artifactId>byte-buddy-agent</artifactId>
- <version>${byte-buddy.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-inline</artifactId>
- <version>${mockito.version}</version>
- <scope>test</scope>
- </dependency>
- <!-- mybatis -->
- <dependency>
- <groupId>org.mybatis.spring.boot</groupId>
- <artifactId>mybatis-spring-boot-starter</artifactId>
- <version>${mybatis-spring.version}</version>
- </dependency>
- <!-- redisson -->
- <dependency>
- <groupId>org.redisson</groupId>
- <artifactId>redisson-spring-boot-starter</artifactId>
- <version>${redisson-spring.version}</version>
- <exclusions>
- <exclusion>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- </exclusion>
- </exclusions>
- <scope>compile</scope>
- </dependency>
- <!-- mysql -->
- <dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- <version>${mysql.version}</version>
- </dependency>
- <!-- 代碼覆蓋率報表-->
- <dependency>
- <groupId>org.jacoco</groupId>
- <artifactId>jacoco-maven-plugin</artifactId>
- <version>${jacoco.version}</version>
- </dependency>
- <!-- junit5 -->
- <dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter</artifactId>
- <version>${junit-jupiter.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.junit.platform</groupId>
- <artifactId>junit-platform-runner</artifactId>
- <version>${junit-platform.version}</version>
- <exclusions>
- <exclusion>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <!-- H2數據庫-->
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <version>${h2.version}</version>
- <scope>test</scope>
- <exclusions>
- <exclusion>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <version>${maven-surefire.version}</version>
- <executions>
- <!--指定在mvn的test階段執行此插件 -->
- <execution>
- <id>test</id>
- <goals>
- <goal>test</goal>
- </goals>
- </execution>
- </executions>
- <configuration>
- <forkMode>once</forkMode>
- <skip>false</skip>
- <includes>
- <include>**/SuiteTest.java</include>
- </includes>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <version>${maven-compiler.version}</version>
- <configuration>
- <source>8</source>
- <target>8</target>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.jacoco</groupId>
- <artifactId>jacoco-maven-plugin</artifactId>
- <version>${jacoco.version}</version>
- <executions>
- <execution>
- <goals>
- <goal>prepare-agent</goal>
- </goals>
- </execution>
- <!-- attached to Maven test phase -->
- <execution>
- <id>report</id>
- <phase>test</phase>
- <goals>
- <goal>report</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
- <reporting>
- <plugins>
- <plugin>
- <groupId>org.jacoco</groupId>
- <artifactId>jacoco-maven-plugin</artifactId>
- <reportSets>
- <reportSet>
- <reports>
- <!-- select non-aggregate reports -->
- <report>report</report>
- </reports>
- </reportSet>
- </reportSets>
- </plugin>
- </plugins>
- </reporting>
maven 運行測試用例是通過調用 maven 的 surefire 插件并 fork 一個子進程來執行用例的。
forkMode 屬性指明是為每個測試創建一個進程還是所有測試共享同一個進程完成,forkMode 設置值有 never、once、always 、pertest 。
- pretest:每一個測試創建一個新進程,為每個測試創建新的JVM進程是單獨測試的最徹底方式,但也是最慢的,不適合持續回歸。
- once:在一個進程中進行所有測試。once 為默認設置,在持續回歸時建議使用默認設置。
- always:在一個進程中并行的運行腳本,Junit4.7 以上版本才可以使用,surefire 的版本要在 2.6 以上提供這個功能,其中 threadCount 執行時,指定可分配的線程數量,只和參數 parallel 配合使用有效,默認為 5。
- never:從不創建新進程進行測試。
環境準備
在項目中 test 目錄下建立測試入口類 TestApplication.java,將外部依賴 Redis 單獨配置到 DependencyConfig.java 中,同時需要在 TestApplication.class 中排除對 Redis 或 Mongodb 的自動注入配置等。
注意:將外部依賴配置到DependencyConfig并不是必要的,此步驟的目的是為了避免每個單元測試類運行時都會重啟 Spring 上下文,可采用 @MockBean 的方式在代碼中引入外部依賴資源替代此方法。
- @Configuration
- public class DependencyConfig {
- @Bean
- public RedissonClient getRedisClient() {
- return Mockito.mock(RedissonClient.class);
- }
- @Bean
- public RestTemplate restTemplate() {
- return Mockito.mock(RestTemplate.class);
- }
- }
接著在測試入口類中通過 @ComponentScan 對主入口啟動類 Application.class 及 RestClientConfig.class 進行排除。
- @SpringBootApplication
- @ComponentScan(excludeFilters = @ComponentScan.Filter(
- type = FilterType.ASSIGNABLE_TYPE,
- classes = {Application.class, RestClientConfig.class}))
- @MapperScan("com.example.mockito.demo.mapper")
- public class TestApplication {
- }
為了不單獨寫重復的代碼,我們一般會把單獨的代碼抽取出來作為一個公共基類,其中 @ExtendWith(SpringExtension.class) 注解目的是告訴 Spring boot 將使用 Junit5 作為運行平臺,如果想買中使用 Junit4 的話,則需要使用 @RunWith(SpringRunner.class) 注解告知用 SpringRunner 運行啟動。
- @SpringBootTest(classes = TestApplication.class)@ExtendWith(SpringExtension.class)
- public abstract class SpringBaseTest {}
準備好配置環境后,我們便可以開始對項目的 Mapper、Service、Web 層進行測試了。
Mapper層測試
對 Mapper 層的測試主要是驗證 SQL 語句及 Mybatis 傳參等準確性。
- server:
- port: 8080
- spring:
- test:
- context:
- cache:
- max-size: 42
- main:
- allow-bean-definition-overriding: true
- datasource:
- url: jdbc:h2:mem:test;MODE=MYSQL;DB_CLOSE_DELAY=-1;INIT=runscript from 'classpath:init.sql'
- username: sa
- password:
- driverClassName: org.h2.Driver
- hikari:
- minimum-idle: 5
- maximum-pool-size: 15
- auto-commit: true
- idle-timeout: 30000
- pool-name: DatebookHikariCP
- max-lifetime: 1800000
- connection-timeout: 10000
- connection-test-query: SELECT 1
- mybatis:
- type-aliases-package: com.example.mockito.demo.domain
- mapper-locations:
- - classpath:mapper/*.xml
對 Mapper 層的測試并沒有采取 Mock 的方式,而是采用 H2 內存數據庫的方式模擬真實數據庫,同時也避免由于測試數據給真實數據庫帶來的影響。
- jdbc:h2:mem:test;MODE=MYSQL;DB_CLOSE_DELAY=-1;INIT=runscript from 'classpath:init.sql'
配置 H2 數據庫信息,同時 INIT 指定在創建連接時會執行類路徑下的 init.sql 即建表 SQL 。
- public class DemoMapperTest extends SpringBaseTest {
- @Resource
- private DemoMapper demoMapper;
- @Test
- public void testInsert() {
- Demo demo = new Demo();
- demo.setName("test");
- demoMapper.insert(demo);
- Integer id = demo.getId();
- Demo model = demoMapper.getDetail(id);
- Assert.assertNotNull(model);
- Assert.assertEquals(demo.getName(), model.getName());
- }
- @Test
- public void testGetList() {
- Demo demo = new Demo();
- demo.setName("test");
- demoMapper.insert(demo);
- List<Demo> demoList = demoMapper.getList();
- Assert.assertNotNull(demoList);
- Assert.assertEquals(1, demoList.size());
- }
- }
Service層測試
一般項目的業務邏輯寫在 service 層,需要寫更多的測試用例驗證業務代碼邏輯性及準確性,盡可能的覆蓋到業務代碼的分支邏輯。
- public class DemoServiceTest extends SpringBaseTest {
- @Resource
- private DemoService demoService;
- @Resource
- private RedissonClient redissonClient;
- @Resource
- private RestTemplate restTemplate;
- @BeforeEach
- public void setUp() {
- MockitoAnnotations.openMocks(this);
- }
- @Test
- public void testGetList() {
- //測試第一個分支邏輯
- RAtomicLong rAtomicLong = Mockito.mock(RAtomicLong.class);
- Mockito.when(redissonClient.getAtomicLong(ArgumentMatchers.anyString())).thenReturn(rAtomicLong);
- long count = 4L;
- Mockito.when(rAtomicLong.incrementAndGet()).thenReturn(count);
- List<Demo> demoList = demoService.getList();
- Assert.assertTrue(demoList != null && demoList.size() == 1);
- Demo demo = demoList.get(0);
- Assert.assertNotNull(demo);
- Assert.assertEquals(Integer.valueOf(4), demo.getId());
- Assert.assertEquals("testCount4", demo.getName());
- //測試第二個分支邏輯
- Mockito.when(redissonClient.getAtomicLong(ArgumentMatchers.anyString())).thenReturn(rAtomicLong);
- count = 1L;
- Mockito.when(rAtomicLong.incrementAndGet()).thenReturn(count);
- MockedStatic<AESUtil> aesUtilMockedStatic = Mockito.mockStatic(AESUtil.class);
- aesUtilMockedStatic.when(() -> AESUtil.encrypt(ArgumentMatchers.eq("test"), ArgumentMatchers.eq("1234567890123456")))
- .thenReturn("demo");
- demoList = demoService.getList();
- Assert.assertTrue(demoList != null && demoList.size() == 1);
- Demo encryptDemo = demoList.get(0);
- Assert.assertNotNull(encryptDemo);
- Assert.assertEquals(Integer.valueOf(1), encryptDemo.getId());
- Assert.assertEquals("testEncrypt", encryptDemo.getName());
- //測試第三個分支邏輯
- Mockito.when(redissonClient.getAtomicLong(ArgumentMatchers.anyString())).thenReturn(rAtomicLong);
- count = 1L;
- Mockito.when(rAtomicLong.incrementAndGet()).thenReturn(count);
- //執行真實方法
- aesUtilMockedStatic.when(() -> AESUtil.encrypt(ArgumentMatchers.eq("test"), ArgumentMatchers.eq("1234567890123456")))
- .thenCallRealMethod();
- String mobileUrl = "https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=";
- MobileInfoDTO mobileInfoDTO = new MobileInfoDTO();
- mobileInfoDTO.setName("testMobile");
- mobileInfoDTO.setLocation("testLocation");
- Mockito.when(restTemplate.getForObject(mobileUrl, MobileInfoDTO.class)).thenReturn(mobileInfoDTO);
- demoList = demoService.getList();
- Assert.assertNotNull(demoList);
- Assert.assertEquals(1, demoList.size());
- Demo demo1 = demoList.get(0);
- Assert.assertNotNull(demo1);
- Assert.assertEquals(mobileInfoDTO.getName(), demo1.getName());
- }
- }
WEB層測試
- public class DemoControllerTest extends SpringBaseTest {
- private MockMvc mockMvc;
- @Mock
- private DemoService demoService;
- //把demoService注入到demoController中
- @InjectMocks
- private DemoController demoController;
- @BeforeEach
- public void setUp() {
- MockitoAnnotations.openMocks(this);
- //手動構建,不使用@AutoConfigureMockMvc注解避免重復啟動spring上下文
- this.mockMvc = MockMvcBuilders.standaloneSetup(demoController).build();
- }
- @Test
- public void addDemoTest() throws Exception {
- String name = "mock demo add";
- mockMvc.perform(MockMvcRequestBuilders.post("/demo")
- .param("name", name))
- .andExpect(MockMvcResultMatchers.status().isOk())
- .andExpect(MockMvcResultMatchers.content().string(containsString("OK")))
- .andDo(MockMvcResultHandlers.print());
- //驗證調用次數,發生過一次
- Mockito.verify(demoService, Mockito.times(1)).addDemo(ArgumentMatchers.any());
- }
- @Test
- public void getDemoListTest() throws Exception {
- mockMvc.perform(MockMvcRequestBuilders.get("/demos"))
- .andExpect(MockMvcResultMatchers.status().isOk())
- .andExpect(MockMvcResultMatchers.content().string("[]"))
- .andDo(MockMvcResultHandlers.print());
- //驗證調用次數,發生過一次
- Mockito.verify(demoService, Mockito.times(1)).getList();
- }
- @Test
- public void getDemoDetailTest() throws Exception {
- String name = "mock demo getDetail";
- mockMvc.perform(MockMvcRequestBuilders.get("/demo/{id}", 1))
- .andExpect(MockMvcResultMatchers.status().isOk())
- .andExpect(MockMvcResultMatchers.content().string(containsString("")))
- .andDo(MockMvcResultHandlers.print());
- //驗證調用次數,發生過一次
- Mockito.verify(demoService, Mockito.times(1)).getDetail(ArgumentMatchers.anyInt());
- }
- }
套件測試
當寫完 mapper、service、web 層的單元測試后,我們便可以把這些單元測試類都打包到套件中,并在 maven surefire 插件指定需要執行的套件類,我們可以使用 @SelectPackages 或 @SelectClass 注解把要測試的類包含進來。
- @RunWith(JUnitPlatform.class)
- @SelectPackages("com.example.mockito.demo")
- public class SuiteTest {
- }
把測試套件配置到surefire插件中。
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <version>${maven-surefire.version}</version>
- <executions>
- <!--指定在mvn的test階段執行此插件 -->
- <execution>
- <id>test</id>
- <goals>
- <goal>test</goal>
- </goals>
- </execution>
- </executions>
- <configuration>
- <forkMode>once</forkMode>
- <skip>false</skip>
- <includes>
- <include>**/SuiteTest.java</include>
- </includes>
- </configuration>
- </plugin>
生成覆蓋率報告
- <plugin>
- <groupId>org.jacoco</groupId>
- <artifactId>jacoco-maven-plugin</artifactId>
- <version>${jacoco.version}</version>
- <executions>
- <execution>
- <goals>
- <goal>prepare-agent</goal>
- </goals>
- </execution>
- <!-- 綁定到test階段 -->
- <execution>
- <id>report</id>
- <phase>test</phase>
- <goals>
- <goal>report</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
項目中使用 jacoco 作為代碼覆蓋率工具,在命令行中運行 mvn clean test 后會執行所有單元測試用例,隨后會在 target 目錄下生成site 文件夾,文件夾包含 jacoco 插件生成的測試報告。
報告中主要包含本次測試中涉及到的類、方法、分支覆蓋率,其中紅色的表示未被覆蓋到,綠色表示全覆蓋,黃色的則表示部分覆蓋到,可點擊某個包或某個類查看具體哪些行未被覆蓋等。
注意:
- test 測試目錄與 main 開發目錄的資源是相互隔離的。
- 使用 @MockBean 會導致啟動 Spring 上下文多次,影響測試效率。
- 使用 @AutoConfigureMockMvc 注解導致 Spring 上下文多次重啟,建議使用MockMvcBuilders.standaloneSetup(demoController).build() 構建。
言而總之
雖然編寫單元測試會帶來一定的工作量,但通過使用 Mockito 不僅可以保留測試用例,還可以快速驗證改動后的代碼邏輯,對復雜或依賴中間件多的項目而言,使用 Mockito 的優勢會更加明顯。
除此之外,更加重要的是我們自己可以創建條件來模擬各種情景下代碼的邏輯準確性,保證代碼的質量,提高代碼維護性、提前發現潛在的問題,不再因為賬號問題等導致測不到某些邏輯代碼,使得項目上線也心里有底。
引用
[1]Mockito2.7.2 及更高版本添加了對 final 類及方法支持: https://www.baeldung.com/mockito-final
[2]Mockito3.4.0 及更高版本支持對靜態方法的 Mock: https://tech.cognifide.com/blog/2020/mocking-static-methods-made-possible-in-mockito-3.4.0






























