自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

外部依賴太多,如何寫(xiě) Java 單元測(cè)試?

開(kāi)發(fā) 后端
Mockito是一個(gè)非常優(yōu)秀的模擬框架,可以使用它簡(jiǎn)潔的API來(lái)編寫(xiě)漂亮的測(cè)試代碼,它的測(cè)試代碼可讀性高同時(shí)會(huì)產(chǎn)生清晰的錯(cuò)誤日志。

[[405509]]

本文轉(zhuǎn)載自微信公眾號(hào)「碼農(nóng)私房話」,作者Liew 。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼農(nóng)私房話公眾號(hào)。

事出有因

在日常的開(kāi)發(fā)中,很多人習(xí)慣性地寫(xiě)完需求代碼后,嗖的一聲用 Postman 模擬真實(shí)請(qǐng)求或?qū)憥讉€(gè) JUnit 的單元測(cè)試跑功能點(diǎn),只要沒(méi)有問(wèn)題就上線了,但其實(shí)這存在很大風(fēng)險(xiǎn),一方面無(wú)法驗(yàn)證業(yè)務(wù)邏輯的不同分支,另外一方面需嚴(yán)重依賴中間件資源才能運(yùn)行測(cè)試用例,占用大量資源。

秣馬厲兵

Mockito是一個(gè)非常優(yōu)秀的模擬框架,可以使用它簡(jiǎn)潔的API來(lái)編寫(xiě)漂亮的測(cè)試代碼,它的測(cè)試代碼可讀性高同時(shí)會(huì)產(chǎn)生清晰的錯(cuò)誤日志。

添加 maven 依賴

  1. <dependency> 
  2.     <groupId>org.mockito</groupId> 
  3.     <artifactId>mockito-core</artifactId> 
  4.     <version>3.3.3</version> 
  5.     <scope>test</scope> 
  6. </dependency> 

 

注意:Mockito 3.X 版本使用了 JDK8 API,但功能與 2.X 版本并沒(méi)有太大的變化。

指定 MockitoJUnitRunner

  1. @RunWith(MockitoJUnitRunner.class) 
  2. public class MockitoDemoTest { 
  3.  
  4.     //注入依賴的資源對(duì)象 
  5.     @Mock 
  6.     private MockitoTestService mockitoTestService; 
  7.     @Before 
  8.     public void before(){ 
  9.         MockitoAnnotations.initMocks(this); 
  10.     } 

從代碼中觀察到,使用 @Mock 注解標(biāo)識(shí)哪些對(duì)象需要被 Mock,同時(shí)在執(zhí)行測(cè)試用例前初始化 MockitoAnnotations.initMocks(this) 告訴框架使 Mock 相關(guān)注解生效。

驗(yàn)證對(duì)象行為 Verify

  1. @Test 
  2. public void testVerify(){ 
  3.     //創(chuàng)建mock 
  4.     List mockedList = mock(List.class); 
  5.     mockedList.add("1"); 
  6.     mockedList.clear(); 
  7.     //驗(yàn)證list調(diào)用過(guò)add的操作行為 
  8.     verify(mockedList).add("1"); 
  9.     //驗(yàn)證list調(diào)用過(guò)clear的操作行為 
  10.     verify(mockedList).clear(); 
  11.     //使用內(nèi)建anyInt()參數(shù)匹配器,并存根 
  12.     when(mockedList.get(anyInt())).thenReturn("element"); 
  13.     System.out.println(mockedList.get(2)); //此處輸出為element 
  14.     verify(mockedList).get(anyInt()); 

存根 stubbing

stubbing 完全是模擬一個(gè)外部依賴、用來(lái)提供測(cè)試時(shí)所需要的數(shù)據(jù)。

  1. @Test 
  2. public void testStub(){ 
  3.     //可以mock具體的類,而不僅僅是接口 
  4.     LinkedList mockedList = mock(LinkedList.class); 
  5.     //存根(stubbing) 
  6.     when(mockedList.get(0)).thenReturn("first"); 
  7.     when(mockedList.get(1)).thenThrow(new RuntimeException()); 
  8.     //下面會(huì)打印 "first" 
  9.     System.out.println(mockedList.get(0)); 
  10.     //下面會(huì)拋出運(yùn)行時(shí)異常 
  11.     System.out.println(mockedList.get(1)); 
  12.     //下面會(huì)打印"null" 因?yàn)間et(999)沒(méi)有存根(stub) 
  13.     System.out.println(mockedList.get(999)); 
  14.     doThrow(new RuntimeException()).when(mockedList).clear(); 
  15.     //下面會(huì)拋出 RuntimeException: 
  16.     mockedList.clear(); 
  • 存根(stub)可以覆蓋,測(cè)試方法可以覆蓋全局設(shè)置的通用存根。
  • 一旦做了存根,無(wú)論這個(gè)方法被調(diào)用多少次,方法將總是返回存根的值。

存根的連續(xù)調(diào)用

  1. @Test 
  2. public void testStub() { 
  3.     when(mock.someMethod("some arg")) 
  4.     .thenThrow(new RuntimeException()) 
  5.     .thenReturn("foo"); 
  6.     mock.someMethod("some arg"); //第一次調(diào)用:拋出運(yùn)行時(shí)異常 
  7.     //第二次調(diào)用: 打印 "foo" 
  8.     System.out.println(mock.someMethod("some arg")); 
  9.     //任何連續(xù)調(diào)用: 還是打印 "foo" (最后的存根生效). 
  10.     System.out.println(mock.someMethod("some arg")); 
  11.     //可供選擇的連續(xù)存根的更短版本: 
  12.     when(mock.someMethod("some arg")).thenReturn("one""two""three"); 
  13.     when(mock.someMethod(anyString())).thenAnswer(new Answer() { 
  14.         Object answer(InvocationOnMock invocation) { 
  15.             Object[] args = invocation.getArguments(); 
  16.             Object mock = invocation.getMock(); 
  17.             return "called with arguments: " + args; 
  18.         } 
  19.     }); 
  20.     // "called with arguments: foo 
  21.     System.out.println(mock.someMethod("foo")); 

在做方法存根時(shí),可以指定不同時(shí)機(jī)需要提供的測(cè)試數(shù)據(jù),例如第一次調(diào)用返回 xxx,第二次調(diào)用時(shí)拋出異常等。

參數(shù)匹配器

  1. @Test 
  2. public void testArugument{ 
  3.     //使用內(nèi)建anyInt()參數(shù)匹配器 
  4.     when(mockedList.get(anyInt())).thenReturn("element"); 
  5.     System.out.println(mockedList.get(999)); //打印 "element" 
  6.     //同樣可以用參數(shù)匹配器做驗(yàn)證 
  7.     verify(mockedList).get(anyInt()); 
  8.  
  9.     //注意:如果使用參數(shù)匹配器,所有的參數(shù)都必須通過(guò)匹配器提供。 
  10.     verify(mock) 
  11.     .someMethod(anyInt(), anyString(), eq("third argument")); 
  12.     //上面是正確的 - eq(0也是參數(shù)匹配器),而下面的是錯(cuò)誤的 
  13.     verify(mock) 
  14.     .someMethod(anyInt(), anyString(), "third argument"); 

驗(yàn)證調(diào)用次數(shù)

  1. @Test 
  2. public void testVerify{ 
  3.     List<String> mockedList = new ArrayList(); 
  4.     mockedList.add("once"); 
  5.     mockedList.add("twice"); 
  6.     mockedList.add("twice"); 
  7.     mockedList.add("three times"); 
  8.     mockedList.add("three times"); 
  9.     mockedList.add("three times"); 
  10.     //下面兩個(gè)驗(yàn)證是等同的 - 默認(rèn)使用times(1) 
  11.     verify(mockedList).add("once"); 
  12.     verify(mockedList, times(1)).add("once"); 
  13.     verify(mockedList, times(2)).add("twice"); 
  14.     verify(mockedList, times(3)).add("three times"); 
  15.     //使用using never()來(lái)驗(yàn)證. never()相當(dāng)于 times(0) 
  16.     verify(mockedList, never()).add("never happened"); 
  17.     //使用 atLeast()/atMost()來(lái)驗(yàn)證 
  18.     verify(mockedList, atLeastOnce()).add("three times"); 
  19.     verify(mockedList, atLeast(2)).add("five times"); 
  20.     verify(mockedList, atMost(5)).add("three times"); 

驗(yàn)證調(diào)用順序

  1. @Test 
  2. public void testOrder() 
  3.     // A. 單個(gè)Mock,方法必須以特定順序調(diào)用 
  4.     List singleMock = mock(List.class); 
  5.  
  6.     //使用單個(gè)Mock 
  7.     singleMock.add("was added first"); 
  8.     singleMock.add("was added second"); 
  9.  
  10.     //為singleMock創(chuàng)建 inOrder 檢驗(yàn)器 
  11.     InOrder inOrder = inOrder(singleMock); 
  12.  
  13.     //確保add方法第一次調(diào)用是用"was added first",然后是用"was added second" 
  14.     inOrder.verify(singleMock).add("was added first"); 
  15.     inOrder.verify(singleMock).add("was added second"); 

以上是 Mockito 框架常用的使用方式,但 Mockito 有一定的局限性, 它只能 Mock 類或者接口,對(duì)于靜態(tài)、私有及final方法的 Mock 則無(wú)能為力了。

而 PowerMock 正是彌補(bǔ)這塊的缺陷,它的實(shí)現(xiàn)原理如下:

  • 當(dāng)某個(gè)測(cè)試方法被注解 @PrepareForTest 標(biāo)注后,在運(yùn)行測(cè)試用例時(shí)會(huì)創(chuàng)建一個(gè)新的 MockClassLoader 實(shí)例并加載該測(cè)試用例使用到的類(系統(tǒng)類除外)。
  • PowerMock 會(huì)根據(jù)你的 mock 要求,去修改寫(xiě)在注解 @PrepareForTest 里的 class 文件內(nèi)容(調(diào)用非系統(tǒng)的靜態(tài)、Final方法),若是包含調(diào)用系統(tǒng)的方法則修改調(diào)用系統(tǒng)方法的類的 class 文件內(nèi)容達(dá)到滿足需求 。

但值得高興的是在 Mockito2.7.2 及更高版本添加了對(duì) final 類及方法支持[1] 。

同樣, Mockito3.4.0 及更高版本支持對(duì)靜態(tài)方法的 Mock[2],雖然是處于孵化階段,但對(duì)于我們做單元測(cè)試而言是已經(jīng)足夠了。

決勝之機(jī)

大多數(shù)項(xiàng)目使用了 Spring 或 Spring Boot 作為基礎(chǔ)框架,研發(fā)只需要關(guān)心業(yè)務(wù)邏輯即可。

在代碼例子中將使用 Junit5 的版本,因此要求 Spring boot版本必須是2.2.0版本或以上,采用 Mockito3.5.11 的版本作為 Mock 框架,減少項(xiàng)目對(duì) PowerMock 的依賴,另外還有一個(gè)重要原因是因?yàn)槟壳癙owerMock不支持 Junit5,無(wú)法在引入 PowerMock 后使用Junit5 的相關(guān)功能及API,本文項(xiàng)目代碼地址:https://github.com/GoQeng/spring-mockito3-demo。

maven 配置

  1. <properties> 
  2.     <java.version>1.8</java.version> 
  3.     <mockito.version>3.5.11</mockito.version> 
  4.     <byte-buddy.version>1.10.15</byte-buddy.version> 
  5.     <redisson-spring.version>3.13.4</redisson-spring.version> 
  6.     <mysql.version>5.1.48</mysql.version> 
  7.     <jacoco.version>0.8.6</jacoco.version> 
  8.     <junit-jupiter.version>5.6.2</junit-jupiter.version> 
  9.     <junit-platform.version>1.1.1</junit-platform.version> 
  10.     <mybatis-spring.version>2.1.3</mybatis-spring.version> 
  11.     <maven-compiler.version>3.8.1</maven-compiler.version> 
  12.     <maven-surefire.version>2.12.4</maven-surefire.version> 
  13.     <h2.version>1.4.197</h2.version> 
  14. </properties> 
  15.  
  16. <dependencies> 
  17.     <!-- spring boot相關(guān)依賴 --> 
  18.     <dependency> 
  19.         <groupId>org.springframework.boot</groupId> 
  20.         <artifactId>spring-boot-starter-web</artifactId> 
  21.     </dependency> 
  22.  
  23.     <dependency> 
  24.         <groupId>org.springframework.boot</groupId> 
  25.         <artifactId>spring-boot-starter-test</artifactId> 
  26.         <scope>test</scope> 
  27.         <exclusions> 
  28.             <exclusion> 
  29.                 <groupId>org.mockito</groupId> 
  30.                 <artifactId>mockito-core</artifactId> 
  31.             </exclusion> 
  32.             <exclusion> 
  33.                 <groupId>org.junit.vintage</groupId> 
  34.                 <artifactId>junit-vintage-engine</artifactId> 
  35.             </exclusion> 
  36.         </exclusions> 
  37.     </dependency> 
  38.  
  39.     <!-- Mockito --> 
  40.     <dependency> 
  41.         <groupId>org.mockito</groupId> 
  42.         <artifactId>mockito-core</artifactId> 
  43.         <version>${mockito.version}</version> 
  44.         <scope>compile</scope> 
  45.         <exclusions> 
  46.             <exclusion> 
  47.                 <groupId>net.bytebuddy</groupId> 
  48.                 <artifactId>byte-buddy</artifactId> 
  49.             </exclusion> 
  50.             <exclusion> 
  51.                 <groupId>net.bytebuddy</groupId> 
  52.                 <artifactId>byte-buddy-agent</artifactId> 
  53.             </exclusion> 
  54.         </exclusions> 
  55.     </dependency> 
  56.     <!-- 由于mockito-core自帶的byte-buddy版本低,無(wú)法使用mock靜態(tài)方法 --> 
  57.     <dependency> 
  58.         <groupId>net.bytebuddy</groupId> 
  59.         <artifactId>byte-buddy</artifactId> 
  60.         <version>${byte-buddy.version}</version> 
  61.     </dependency> 
  62.  
  63.     <dependency> 
  64.         <groupId>net.bytebuddy</groupId> 
  65.         <artifactId>byte-buddy-agent</artifactId> 
  66.         <version>${byte-buddy.version}</version> 
  67.         <scope>test</scope> 
  68.     </dependency> 
  69.  
  70.     <dependency> 
  71.         <groupId>org.mockito</groupId> 
  72.         <artifactId>mockito-inline</artifactId> 
  73.         <version>${mockito.version}</version> 
  74.         <scope>test</scope> 
  75.     </dependency> 
  76.  
  77.     <!-- mybatis --> 
  78.     <dependency> 
  79.         <groupId>org.mybatis.spring.boot</groupId> 
  80.         <artifactId>mybatis-spring-boot-starter</artifactId> 
  81.         <version>${mybatis-spring.version}</version> 
  82.     </dependency> 
  83.  
  84.     <!-- redisson --> 
  85.     <dependency> 
  86.         <groupId>org.redisson</groupId> 
  87.         <artifactId>redisson-spring-boot-starter</artifactId> 
  88.         <version>${redisson-spring.version}</version> 
  89.         <exclusions> 
  90.             <exclusion> 
  91.                 <groupId>junit</groupId> 
  92.                 <artifactId>junit</artifactId> 
  93.             </exclusion> 
  94.         </exclusions> 
  95.         <scope>compile</scope> 
  96.     </dependency> 
  97.  
  98.     <!-- mysql --> 
  99.     <dependency> 
  100.         <groupId>mysql</groupId> 
  101.         <artifactId>mysql-connector-java</artifactId> 
  102.         <version>${mysql.version}</version> 
  103.     </dependency> 
  104.  
  105.     <!-- 代碼覆蓋率報(bào)表--> 
  106.     <dependency> 
  107.         <groupId>org.jacoco</groupId> 
  108.         <artifactId>jacoco-maven-plugin</artifactId> 
  109.         <version>${jacoco.version}</version> 
  110.     </dependency> 
  111.  
  112.     <!-- junit5 --> 
  113.     <dependency> 
  114.         <groupId>org.junit.jupiter</groupId> 
  115.         <artifactId>junit-jupiter</artifactId> 
  116.         <version>${junit-jupiter.version}</version> 
  117.         <scope>test</scope> 
  118.     </dependency> 
  119.  
  120.     <dependency> 
  121.         <groupId>org.junit.platform</groupId> 
  122.         <artifactId>junit-platform-runner</artifactId> 
  123.         <version>${junit-platform.version}</version> 
  124.         <exclusions> 
  125.             <exclusion> 
  126.                 <groupId>junit</groupId> 
  127.                 <artifactId>junit</artifactId> 
  128.             </exclusion> 
  129.         </exclusions> 
  130.     </dependency> 
  131.  
  132.     <!-- H2數(shù)據(jù)庫(kù)--> 
  133.     <dependency> 
  134.         <groupId>com.h2database</groupId> 
  135.         <artifactId>h2</artifactId> 
  136.         <version>${h2.version}</version> 
  137.         <scope>test</scope> 
  138.         <exclusions> 
  139.             <exclusion> 
  140.                 <groupId>junit</groupId> 
  141.                 <artifactId>junit</artifactId> 
  142.             </exclusion> 
  143.         </exclusions> 
  144.     </dependency> 
  145. </dependencies> 
  146.  
  147. <build> 
  148.     <plugins> 
  149.         <plugin> 
  150.             <groupId>org.apache.maven.plugins</groupId> 
  151.             <artifactId>maven-surefire-plugin</artifactId> 
  152.             <version>${maven-surefire.version}</version> 
  153.             <executions> 
  154.                 <!--指定在mvn的test階段執(zhí)行此插件 --> 
  155.                 <execution> 
  156.                     <id>test</id> 
  157.                     <goals> 
  158.                         <goal>test</goal> 
  159.                     </goals> 
  160.                 </execution> 
  161.             </executions> 
  162.             <configuration> 
  163.                 <forkMode>once</forkMode> 
  164.                 <skip>false</skip> 
  165.                 <includes> 
  166.                     <include>**/SuiteTest.java</include> 
  167.                 </includes> 
  168.             </configuration> 
  169.         </plugin> 
  170.         <plugin> 
  171.             <groupId>org.apache.maven.plugins</groupId> 
  172.             <artifactId>maven-compiler-plugin</artifactId> 
  173.             <version>${maven-compiler.version}</version> 
  174.             <configuration> 
  175.                 <source>8</source> 
  176.                 <target>8</target> 
  177.             </configuration> 
  178.         </plugin> 
  179.         <plugin> 
  180.             <groupId>org.jacoco</groupId> 
  181.             <artifactId>jacoco-maven-plugin</artifactId> 
  182.             <version>${jacoco.version}</version> 
  183.             <executions> 
  184.                 <execution> 
  185.                     <goals> 
  186.                         <goal>prepare-agent</goal> 
  187.                     </goals> 
  188.                 </execution> 
  189.                 <!-- attached to Maven test phase --> 
  190.                 <execution> 
  191.                     <id>report</id> 
  192.                     <phase>test</phase> 
  193.                     <goals> 
  194.                         <goal>report</goal> 
  195.                     </goals> 
  196.                 </execution> 
  197.             </executions> 
  198.         </plugin> 
  199.     </plugins> 
  200. </build> 
  201. <reporting> 
  202.     <plugins> 
  203.         <plugin> 
  204.             <groupId>org.jacoco</groupId> 
  205.             <artifactId>jacoco-maven-plugin</artifactId> 
  206.             <reportSets> 
  207.                 <reportSet> 
  208.                     <reports> 
  209.                         <!-- select non-aggregate reports --> 
  210.                         <report>report</report> 
  211.                     </reports> 
  212.                 </reportSet> 
  213.             </reportSets> 
  214.         </plugin> 
  215.     </plugins> 
  216. </reporting> 

 

 

 

 

 

 

maven 運(yùn)行測(cè)試用例是通過(guò)調(diào)用 maven 的 surefire 插件并 fork 一個(gè)子進(jìn)程來(lái)執(zhí)行用例的。

forkMode 屬性指明是為每個(gè)測(cè)試創(chuàng)建一個(gè)進(jìn)程還是所有測(cè)試共享同一個(gè)進(jìn)程完成,forkMode 設(shè)置值有 never、once、always 、pertest 。

  • pretest:每一個(gè)測(cè)試創(chuàng)建一個(gè)新進(jìn)程,為每個(gè)測(cè)試創(chuàng)建新的JVM進(jìn)程是單獨(dú)測(cè)試的最徹底方式,但也是最慢的,不適合持續(xù)回歸。
  • once:在一個(gè)進(jìn)程中進(jìn)行所有測(cè)試。once 為默認(rèn)設(shè)置,在持續(xù)回歸時(shí)建議使用默認(rèn)設(shè)置。
  • always:在一個(gè)進(jìn)程中并行的運(yùn)行腳本,Junit4.7 以上版本才可以使用,surefire 的版本要在 2.6 以上提供這個(gè)功能,其中 threadCount 執(zhí)行時(shí),指定可分配的線程數(shù)量,只和參數(shù) parallel 配合使用有效,默認(rèn)為 5。
  • never:從不創(chuàng)建新進(jìn)程進(jìn)行測(cè)試。

環(huán)境準(zhǔn)備

在項(xiàng)目中 test 目錄下建立測(cè)試入口類 TestApplication.java,將外部依賴 Redis 單獨(dú)配置到 DependencyConfig.java 中,同時(shí)需要在 TestApplication.class 中排除對(duì) Redis 或 Mongodb 的自動(dòng)注入配置等。

注意:將外部依賴配置到DependencyConfig并不是必要的,此步驟的目的是為了避免每個(gè)單元測(cè)試類運(yùn)行時(shí)都會(huì)重啟 Spring 上下文,可采用 @MockBean 的方式在代碼中引入外部依賴資源替代此方法。

  1. @Configuration 
  2. public class DependencyConfig { 
  3.  
  4.     @Bean 
  5.     public RedissonClient getRedisClient() { 
  6.         return Mockito.mock(RedissonClient.class); 
  7.     } 
  8.  
  9.     @Bean 
  10.     public RestTemplate restTemplate() { 
  11.         return Mockito.mock(RestTemplate.class); 
  12.     } 

接著在測(cè)試入口類中通過(guò) @ComponentScan 對(duì)主入口啟動(dòng)類 Application.class 及 RestClientConfig.class 進(jìn)行排除。

  1. @SpringBootApplication 
  2. @ComponentScan(excludeFilters = @ComponentScan.Filter( 
  3.         type = FilterType.ASSIGNABLE_TYPE, 
  4.         classes = {Application.class, RestClientConfig.class})) 
  5. @MapperScan("com.example.mockito.demo.mapper"
  6. public class TestApplication { 
  7.  

為了不單獨(dú)寫(xiě)重復(fù)的代碼,我們一般會(huì)把單獨(dú)的代碼抽取出來(lái)作為一個(gè)公共基類,其中 @ExtendWith(SpringExtension.class) 注解目的是告訴 Spring boot 將使用 Junit5 作為運(yùn)行平臺(tái),如果想買(mǎi)中使用 Junit4 的話,則需要使用 @RunWith(SpringRunner.class) 注解告知用 SpringRunner 運(yùn)行啟動(dòng)。

  1. @SpringBootTest(classes = TestApplication.class)@ExtendWith(SpringExtension.class) 
  2. public abstract class SpringBaseTest {} 

準(zhǔn)備好配置環(huán)境后,我們便可以開(kāi)始對(duì)項(xiàng)目的 Mapper、Service、Web 層進(jìn)行測(cè)試了。

Mapper層測(cè)試

對(duì) Mapper 層的測(cè)試主要是驗(yàn)證 SQL 語(yǔ)句及 Mybatis 傳參等準(zhǔn)確性。

  1. server: 
  2.   port: 8080 
  3. spring: 
  4.   test: 
  5.     context: 
  6.       cache: 
  7.         max-size: 42 
  8.   main: 
  9.     allow-bean-definition-overriding: true 
  10.   datasource: 
  11.     url: jdbc:h2:mem:test;MODE=MYSQL;DB_CLOSE_DELAY=-1;INIT=runscript from 'classpath:init.sql' 
  12.     username: sa 
  13.     password
  14.     driverClassName: org.h2.Driver 
  15.     hikari: 
  16.       minimum-idle: 5 
  17.       maximum-pool-size: 15 
  18.       auto-committrue 
  19.       idle-timeout: 30000 
  20.       pool-name: DatebookHikariCP 
  21.       max-lifetime: 1800000 
  22.       connection-timeout: 10000 
  23.       connection-test-query: SELECT 1 
  24.  
  25. mybatis: 
  26.   type-aliases-package: com.example.mockito.demo.domain 
  27.   mapper-locations: 
  28.     - classpath:mapper/*.xml 

對(duì) Mapper 層的測(cè)試并沒(méi)有采取 Mock 的方式,而是采用 H2 內(nèi)存數(shù)據(jù)庫(kù)的方式模擬真實(shí)數(shù)據(jù)庫(kù),同時(shí)也避免由于測(cè)試數(shù)據(jù)給真實(shí)數(shù)據(jù)庫(kù)帶來(lái)的影響。

  1. jdbc:h2:mem:test;MODE=MYSQL;DB_CLOSE_DELAY=-1;INIT=runscript from 'classpath:init.sql' 

配置 H2 數(shù)據(jù)庫(kù)信息,同時(shí) INIT 指定在創(chuàng)建連接時(shí)會(huì)執(zhí)行類路徑下的 init.sql 即建表 SQL 。

  1. public class DemoMapperTest extends SpringBaseTest { 
  2.  
  3.     @Resource 
  4.     private DemoMapper demoMapper; 
  5.  
  6.     @Test 
  7.     public void testInsert() { 
  8.         Demo demo = new Demo(); 
  9.         demo.setName("test"); 
  10.         demoMapper.insert(demo); 
  11.  
  12.         Integer id = demo.getId(); 
  13.         Demo model = demoMapper.getDetail(id); 
  14.         Assert.assertNotNull(model); 
  15.         Assert.assertEquals(demo.getName(), model.getName()); 
  16.     } 
  17.  
  18.     @Test 
  19.     public void testGetList() { 
  20.         Demo demo = new Demo(); 
  21.         demo.setName("test"); 
  22.         demoMapper.insert(demo); 
  23.  
  24.         List<Demo> demoList = demoMapper.getList(); 
  25.         Assert.assertNotNull(demoList); 
  26.         Assert.assertEquals(1, demoList.size()); 
  27.     } 

Service層測(cè)試

一般項(xiàng)目的業(yè)務(wù)邏輯寫(xiě)在 service 層,需要寫(xiě)更多的測(cè)試用例驗(yàn)證業(yè)務(wù)代碼邏輯性及準(zhǔn)確性,盡可能的覆蓋到業(yè)務(wù)代碼的分支邏輯。

  1. public class DemoServiceTest extends SpringBaseTest { 
  2.  
  3.   @Resource 
  4.   private DemoService demoService; 
  5.   @Resource 
  6.   private RedissonClient redissonClient; 
  7.  
  8.   @Resource 
  9.   private RestTemplate restTemplate; 
  10.  
  11.   @BeforeEach 
  12.   public void setUp() { 
  13.       MockitoAnnotations.openMocks(this); 
  14.   } 
  15.  
  16.   @Test 
  17.   public void testGetList() { 
  18.       //測(cè)試第一個(gè)分支邏輯 
  19.       RAtomicLong rAtomicLong = Mockito.mock(RAtomicLong.class); 
  20.       Mockito.when(redissonClient.getAtomicLong(ArgumentMatchers.anyString())).thenReturn(rAtomicLong); 
  21.       long count = 4L; 
  22.       Mockito.when(rAtomicLong.incrementAndGet()).thenReturn(count); 
  23.       List<Demo> demoList = demoService.getList(); 
  24.       Assert.assertTrue(demoList != null && demoList.size() == 1); 
  25.       Demo demo = demoList.get(0); 
  26.       Assert.assertNotNull(demo); 
  27.       Assert.assertEquals(Integer.valueOf(4), demo.getId()); 
  28.       Assert.assertEquals("testCount4", demo.getName()); 
  29.  
  30.       //測(cè)試第二個(gè)分支邏輯 
  31.       Mockito.when(redissonClient.getAtomicLong(ArgumentMatchers.anyString())).thenReturn(rAtomicLong); 
  32.       count = 1L; 
  33.       Mockito.when(rAtomicLong.incrementAndGet()).thenReturn(count); 
  34.  
  35.       MockedStatic<AESUtil> aesUtilMockedStatic = Mockito.mockStatic(AESUtil.class); 
  36.       aesUtilMockedStatic.when(() -> AESUtil.encrypt(ArgumentMatchers.eq("test"), ArgumentMatchers.eq("1234567890123456"))) 
  37.               .thenReturn("demo"); 
  38.  
  39.       demoList = demoService.getList(); 
  40.       Assert.assertTrue(demoList != null && demoList.size() == 1); 
  41.       Demo encryptDemo = demoList.get(0); 
  42.       Assert.assertNotNull(encryptDemo); 
  43.       Assert.assertEquals(Integer.valueOf(1), encryptDemo.getId()); 
  44.       Assert.assertEquals("testEncrypt", encryptDemo.getName()); 
  45.  
  46.       //測(cè)試第三個(gè)分支邏輯 
  47.       Mockito.when(redissonClient.getAtomicLong(ArgumentMatchers.anyString())).thenReturn(rAtomicLong); 
  48.       count = 1L; 
  49.       Mockito.when(rAtomicLong.incrementAndGet()).thenReturn(count); 
  50.  
  51.       //執(zhí)行真實(shí)方法 
  52.       aesUtilMockedStatic.when(() -> AESUtil.encrypt(ArgumentMatchers.eq("test"), ArgumentMatchers.eq("1234567890123456"))) 
  53.               .thenCallRealMethod(); 
  54.  
  55.       String mobileUrl = "https://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel="
  56.       MobileInfoDTO mobileInfoDTO = new MobileInfoDTO(); 
  57.       mobileInfoDTO.setName("testMobile"); 
  58.       mobileInfoDTO.setLocation("testLocation"); 
  59.       Mockito.when(restTemplate.getForObject(mobileUrl, MobileInfoDTO.class)).thenReturn(mobileInfoDTO); 
  60.       demoList = demoService.getList(); 
  61.       Assert.assertNotNull(demoList); 
  62.       Assert.assertEquals(1, demoList.size()); 
  63.       Demo demo1 = demoList.get(0); 
  64.       Assert.assertNotNull(demo1); 
  65.       Assert.assertEquals(mobileInfoDTO.getName(), demo1.getName()); 
  66.     } 

WEB層測(cè)試

  1. public class DemoControllerTest extends SpringBaseTest { 
  2.  
  3.   private MockMvc mockMvc; 
  4.  
  5.   @Mock 
  6.   private DemoService demoService; 
  7.  
  8.   //把demoService注入到demoController中 
  9.   @InjectMocks 
  10.   private DemoController demoController; 
  11.  
  12.   @BeforeEach 
  13.   public void setUp() { 
  14.       MockitoAnnotations.openMocks(this); 
  15.       //手動(dòng)構(gòu)建,不使用@AutoConfigureMockMvc注解避免重復(fù)啟動(dòng)spring上下文 
  16.       this.mockMvc = MockMvcBuilders.standaloneSetup(demoController).build(); 
  17.   } 
  18.  
  19.   @Test 
  20.   public void addDemoTest() throws Exception { 
  21.       String name = "mock demo add"
  22.       mockMvc.perform(MockMvcRequestBuilders.post("/demo"
  23.               .param("name"name)) 
  24.               .andExpect(MockMvcResultMatchers.status().isOk()) 
  25.               .andExpect(MockMvcResultMatchers.content().string(containsString("OK"))) 
  26.               .andDo(MockMvcResultHandlers.print()); 
  27.       //驗(yàn)證調(diào)用次數(shù),發(fā)生過(guò)一次 
  28.       Mockito.verify(demoService, Mockito.times(1)).addDemo(ArgumentMatchers.any()); 
  29.   } 
  30.  
  31.   @Test 
  32.   public void getDemoListTest() throws Exception { 
  33.       mockMvc.perform(MockMvcRequestBuilders.get("/demos")) 
  34.               .andExpect(MockMvcResultMatchers.status().isOk()) 
  35.               .andExpect(MockMvcResultMatchers.content().string("[]")) 
  36.               .andDo(MockMvcResultHandlers.print()); 
  37.       //驗(yàn)證調(diào)用次數(shù),發(fā)生過(guò)一次 
  38.       Mockito.verify(demoService, Mockito.times(1)).getList(); 
  39.   } 
  40.  
  41.   @Test 
  42.   public void getDemoDetailTest() throws Exception { 
  43.       String name = "mock demo getDetail"
  44.       mockMvc.perform(MockMvcRequestBuilders.get("/demo/{id}", 1)) 
  45.               .andExpect(MockMvcResultMatchers.status().isOk()) 
  46.               .andExpect(MockMvcResultMatchers.content().string(containsString(""))) 
  47.               .andDo(MockMvcResultHandlers.print()); 
  48.       //驗(yàn)證調(diào)用次數(shù),發(fā)生過(guò)一次 
  49.       Mockito.verify(demoService, Mockito.times(1)).getDetail(ArgumentMatchers.anyInt()); 
  50.   } 

套件測(cè)試

當(dāng)寫(xiě)完 mapper、service、web 層的單元測(cè)試后,我們便可以把這些單元測(cè)試類都打包到套件中,并在 maven surefire 插件指定需要執(zhí)行的套件類,我們可以使用 @SelectPackages 或 @SelectClass 注解把要測(cè)試的類包含進(jìn)來(lái)。

  1. @RunWith(JUnitPlatform.class) 
  2. @SelectPackages("com.example.mockito.demo"
  3. public class SuiteTest { 

把測(cè)試套件配置到surefire插件中。

  1. <plugin> 
  2.     <groupId>org.apache.maven.plugins</groupId> 
  3.     <artifactId>maven-surefire-plugin</artifactId> 
  4.     <version>${maven-surefire.version}</version> 
  5.     <executions> 
  6.         <!--指定在mvn的test階段執(zhí)行此插件 --> 
  7.         <execution> 
  8.             <id>test</id> 
  9.             <goals> 
  10.                 <goal>test</goal> 
  11.             </goals> 
  12.         </execution> 
  13.     </executions> 
  14.     <configuration> 
  15.         <forkMode>once</forkMode> 
  16.         <skip>false</skip> 
  17.         <includes> 
  18.             <include>**/SuiteTest.java</include> 
  19.         </includes> 
  20.     </configuration> 
  21. </plugin> 

 

 

 

 

生成覆蓋率報(bào)告

  1. <plugin> 
  2.     <groupId>org.jacoco</groupId> 
  3.     <artifactId>jacoco-maven-plugin</artifactId> 
  4.     <version>${jacoco.version}</version> 
  5.     <executions> 
  6.         <execution> 
  7.             <goals> 
  8.                 <goal>prepare-agent</goal> 
  9.             </goals> 
  10.         </execution> 
  11.         <!-- 綁定到test階段 --> 
  12.         <execution> 
  13.             <id>report</id> 
  14.             <phase>test</phase> 
  15.             <goals> 
  16.                 <goal>report</goal> 
  17.             </goals> 
  18.         </execution> 
  19.     </executions> 
  20. </plugin> 

項(xiàng)目中使用 jacoco 作為代碼覆蓋率工具,在命令行中運(yùn)行 mvn clean test 后會(huì)執(zhí)行所有單元測(cè)試用例,隨后會(huì)在 target 目錄下生成site 文件夾,文件夾包含 jacoco 插件生成的測(cè)試報(bào)告。

報(bào)告中主要包含本次測(cè)試中涉及到的類、方法、分支覆蓋率,其中紅色的表示未被覆蓋到,綠色表示全覆蓋,黃色的則表示部分覆蓋到,可點(diǎn)擊某個(gè)包或某個(gè)類查看具體哪些行未被覆蓋等。

注意:

  • test 測(cè)試目錄與 main 開(kāi)發(fā)目錄的資源是相互隔離的。
  • 使用 @MockBean 會(huì)導(dǎo)致啟動(dòng) Spring 上下文多次,影響測(cè)試效率。
  • 使用 @AutoConfigureMockMvc 注解導(dǎo)致 Spring 上下文多次重啟,建議使用MockMvcBuilders.standaloneSetup(demoController).build() 構(gòu)建。

言而總之

雖然編寫(xiě)單元測(cè)試會(huì)帶來(lái)一定的工作量,但通過(guò)使用 Mockito 不僅可以保留測(cè)試用例,還可以快速驗(yàn)證改動(dòng)后的代碼邏輯,對(duì)復(fù)雜或依賴中間件多的項(xiàng)目而言,使用 Mockito 的優(yōu)勢(shì)會(huì)更加明顯。

除此之外,更加重要的是我們自己可以創(chuàng)建條件來(lái)模擬各種情景下代碼的邏輯準(zhǔn)確性,保證代碼的質(zhì)量,提高代碼維護(hù)性、提前發(fā)現(xiàn)潛在的問(wèn)題,不再因?yàn)橘~號(hào)問(wèn)題等導(dǎo)致測(cè)不到某些邏輯代碼,使得項(xiàng)目上線也心里有底。

引用

[1]Mockito2.7.2 及更高版本添加了對(duì) final 類及方法支持: https://www.baeldung.com/mockito-final

 

[2]Mockito3.4.0 及更高版本支持對(duì)靜態(tài)方法的 Mock: https://tech.cognifide.com/blog/2020/mocking-static-methods-made-possible-in-mockito-3.4.0

 

責(zé)任編輯:武曉燕 來(lái)源: 碼農(nóng)私房話
相關(guān)推薦

2020-09-30 08:08:15

單元測(cè)試應(yīng)用

2022-08-26 08:53:46

單元測(cè)試校驗(yàn)框架

2020-09-11 16:00:40

Bash單元測(cè)試

2021-10-12 19:16:26

Jest單元測(cè)試

2017-01-14 23:42:49

單元測(cè)試框架軟件測(cè)試

2024-01-09 08:08:12

Go單元測(cè)試系統(tǒng)

2011-04-18 13:20:40

單元測(cè)試軟件測(cè)試

2024-08-21 08:22:33

2022-03-15 11:55:24

前端單元測(cè)試

2017-01-14 23:26:17

單元測(cè)試JUnit測(cè)試

2017-01-16 12:12:29

單元測(cè)試JUnit

2021-03-28 23:03:50

Python程序員編碼

2020-07-07 07:33:12

Java單元集成

2020-08-18 08:10:02

單元測(cè)試Java

2017-03-23 16:02:10

Mock技術(shù)單元測(cè)試

2021-05-05 11:38:40

TestNGPowerMock單元測(cè)試

2023-07-28 10:27:48

Java單元測(cè)試

2021-03-11 12:33:50

JavaPowerMock技巧

2023-07-26 08:58:45

Golang單元測(cè)試

2011-07-04 18:16:42

單元測(cè)試
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)