日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關咨詢
選擇下列產(chǎn)品馬上在線溝通
服務時間:8:30-17:00
你可能遇到了下面的問題
關閉右側工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
外部依賴太多,如何寫 Java 單元測試?

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

目前創(chuàng)新互聯(lián)公司已為1000多家的企業(yè)提供了網(wǎng)站建設、域名、雅安服務器托管、網(wǎng)站托管、企業(yè)網(wǎng)站設計、河北網(wǎng)站維護等服務,公司將堅持客戶導向、應用為本的策略,正道將秉承"和諧、參與、激情"的文化,與客戶和合作伙伴齊心協(xié)力一起成長,共同發(fā)展。

事出有因

在日常的開發(fā)中,很多人習慣性地寫完需求代碼后,嗖的一聲用 Postman 模擬真實請求或寫幾個 JUnit 的單元測試跑功能點,只要沒有問題就上線了,但其實這存在很大風險,一方面無法驗證業(yè)務邏輯的不同分支,另外一方面需嚴重依賴中間件資源才能運行測試用例,占用大量資源。

秣馬厲兵

Mockito是一個非常優(yōu)秀的模擬框架,可以使用它簡潔的API來編寫漂亮的測試代碼,它的測試代碼可讀性高同時會產(chǎn)生清晰的錯誤日志。

添加 maven 依賴

 
 
 
 
  1.  
  2.     org.mockito 
  3.     mockito-core 
  4.     3.3.3 
  5.     test 
  6.  

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

指定 MockitoJUnitRunner

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

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

驗證對象行為 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.     //驗證list調用過add的操作行為 
  8.     verify(mockedList).add("1"); 
  9.     //驗證list調用過clear的操作行為 
  10.     verify(mockedList).clear(); 
  11.     //使用內建anyInt()參數(shù)匹配器,并存根 
  12.     when(mockedList.get(anyInt())).thenReturn("element"); 
  13.     System.out.println(mockedList.get(2)); //此處輸出為element 
  14.     verify(mockedList).get(anyInt()); 

存根 stubbing

stubbing 完全是模擬一個外部依賴、用來提供測試時所需要的數(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.     //下面會打印 "first" 
  9.     System.out.println(mockedList.get(0)); 
  10.     //下面會拋出運行時異常 
  11.     System.out.println(mockedList.get(1)); 
  12.     //下面會打印"null" 因為get(999)沒有存根(stub) 
  13.     System.out.println(mockedList.get(999)); 
  14.     doThrow(new RuntimeException()).when(mockedList).clear(); 
  15.     //下面會拋出 RuntimeException: 
  16.     mockedList.clear(); 
  • 存根(stub)可以覆蓋,測試方法可以覆蓋全局設置的通用存根。
  • 一旦做了存根,無論這個方法被調用多少次,方法將總是返回存根的值。

存根的連續(xù)調用

 
 
 
 
  1. @Test 
  2. public void testStub() { 
  3.     when(mock.someMethod("some arg")) 
  4.     .thenThrow(new RuntimeException()) 
  5.     .thenReturn("foo"); 
  6.     mock.someMethod("some arg"); //第一次調用:拋出運行時異常 
  7.     //第二次調用: 打印 "foo" 
  8.     System.out.println(mock.someMethod("some arg")); 
  9.     //任何連續(xù)調用: 還是打印 "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ù)據(jù),例如第一次調用返回 xxx,第二次調用時拋出異常等。

參數(shù)匹配器

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

驗證調用次數(shù)

 
 
 
 
  1. @Test 
  2. public void testVerify{ 
  3.     List 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.     //下面兩個驗證是等同的 - 默認使用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()來驗證. never()相當于 times(0) 
  16.     verify(mockedList, never()).add("never happened"); 
  17.     //使用 atLeast()/atMost()來驗證 
  18.     verify(mockedList, atLeastOnce()).add("three times"); 
  19.     verify(mockedList, atLeast(2)).add("five times"); 
  20.     verify(mockedList, atMost(5)).add("three times"); 

驗證調用順序

 
 
 
 
  1. @Test 
  2. public void testOrder() 
  3.     // A. 單個Mock,方法必須以特定順序調用 
  4.     List singleMock = mock(List.class); 
  5.  
  6.     //使用單個Mock 
  7.     singleMock.add("was added first"); 
  8.     singleMock.add("was added second"); 
  9.  
  10.     //為singleMock創(chuàng)建 inOrder 檢驗器 
  11.     InOrder inOrder = inOrder(singleMock); 
  12.  
  13.     //確保add方法第一次調用是用"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 類或者接口,對于靜態(tài)、私有及final方法的 Mock 則無能為力了。

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

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

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

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

決勝之機

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

在代碼例子中將使用 Junit5 的版本,因此要求 Spring boot版本必須是2.2.0版本或以上,采用 Mockito3.5.11 的版本作為 Mock 框架,減少項目對 PowerMock 的依賴,另外還有一個重要原因是因為目前PowerMock不支持 Junit5,無法在引入 PowerMock 后使用Junit5 的相關功能及API,本文項目代碼地址:https://github.com/GoQeng/spring-mockito3-demo。

maven 配置

 
 
 
 
  1.  
  2.     1.8 
  3.     3.5.11 
  4.     1.10.15 
  5.     3.13.4 
  6.     5.1.48 
  7.     0.8.6 
  8.     5.6.2 
  9.     1.1.1 
  10.     2.1.3 
  11.     3.8.1 
  12.     2.12.4 
  13.     1.4.197 
  14.  
  15.  
  16.  
  17.      
  18.      
  19.         org.springframework.boot 
  20.         spring-boot-starter-web 
  21.      
  22.  
  23.      
  24.         org.springframework.boot 
  25.         spring-boot-starter-test 
  26.         test 
  27.          
  28.              
  29.                 org.mockito 
  30.                 mockito-core 
  31.              
  32.              
  33.                 org.junit.vintage 
  34.                 junit-vintage-engine 
  35.              
  36.          
  37.      
  38.  
  39.      
  40.      
  41.         org.mockito 
  42.         mockito-core 
  43.         ${mockito.version} 
  44.         compile 
  45.          
  46.              
  47.                 net.bytebuddy 
  48.                 byte-buddy 
  49.              
  50.              
  51.                 net.bytebuddy 
  52.                 byte-buddy-agent 
  53.              
  54.          
  55.      
  56.      
  57.      
  58.         net.bytebuddy 
  59.         byte-buddy 
  60.         ${byte-buddy.version} 
  61.      
  62.  
  63.      
  64.         net.bytebuddy 
  65.         byte-buddy-agent 
  66.         ${byte-buddy.version} 
  67.         test 
  68.      
  69.  
  70.      
  71.         org.mockito 
  72.         mockito-inline 
  73.         ${mockito.version} 
  74.         test 
  75.      
  76.  
  77.      
  78.      
  79.         org.mybatis.spring.boot 
  80.         mybatis-spring-boot-starter 
  81.         ${mybatis-spring.version} 
  82.      
  83.  
  84.      
  85.      
  86.         org.redisson 
  87.         redisson-spring-boot-starter 
  88.         ${redisson-spring.version} 
  89.          
  90.              
  91.                 junit 
  92.                 junit 
  93.              
  94.          
  95.         compile 
  96.      
  97.  
  98.      
  99.      
  100.         mysql 
  101.         mysql-connector-java 
  102.         ${mysql.version} 
  103.      
  104.  
  105.      
  106.      
  107.         org.jacoco 
  108.         jacoco-maven-plugin 
  109.         ${jacoco.version} 
  110.      
  111.  
  112.      
  113.      
  114.         org.junit.jupiter 
  115.         junit-jupiter 
  116.         ${junit-jupiter.version} 
  117.         test 
  118.      
  119.  
  120.      
  121.         org.junit.platform 
  122.         junit-platform-runner 
  123.         ${junit-platform.version} 
  124.          
  125.              
  126.                 junit 
  127.                 junit 
  128.              
  129.          
  130.      
  131.  
  132.      
  133.      
  134.         com.h2database 
  135.         h2 
  136.         ${h2.version} 
  137.         test 
  138.          
  139.              
  140.                 junit 
  141.                 junit 
  142.              
  143.          
  144.      
  145.  
  146.  
  147.  
  148.      
  149.          
  150.             org.apache.maven.plugins 
  151.             maven-surefire-plugin 
  152.             ${maven-surefire.version} 
  153.              
  154.                  
  155.                  
  156.                     test 
  157.                      
  158.                         test 
  159.                      
  160.                  
  161.              
  162.              
  163.                 once 
  164.                 false 
  165.                  
  166.                     **/SuiteTest.java 
  167.                  
  168.              
  169.          
  170.          
  171.             org.apache.maven.plugins 
  172.             maven-compiler-plugin 
  173.             ${maven-compiler.version} 
  174.              
  175.                 
  176.                 8 
  177.              
  178.          
  179.          
  180.             org.jacoco 
  181.             jacoco-maven-plugin 
  182.             ${jacoco.version} 
  183.              
  184.                  
  185.                      
  186.                         prepare-agent 
  187.                      
  188.                  
  189.                  
  190.                  
  191.                     report 
  192.                     test 
  193.                      
  194.                         report 
  195.                      
  196.                  
  197.              
  198.          
  199.      
  200.  
  201.  
  202.      
  203.          
  204.             org.jacoco 
  205.             jacoco-maven-plugin 
  206.              
  207.                  
  208.                      
  209.                          
  210.                         report 
  211.                      
  212.                  
  213.              
  214.          
  215.      
  216.  

maven 運行測試用例是通過調用 maven 的 surefire 插件并 fork 一個子進程來執(zhí)行用例的。

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

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

環(huán)境準備

在項目中 test 目錄下建立測試入口類 TestApplication.java,將外部依賴 Redis 單獨配置到 DependencyConfig.java 中,同時需要在 TestApplication.class 中排除對 Redis 或 Mongodb 的自動注入配置等。

注意:將外部依賴配置到DependencyConfig并不是必要的,此步驟的目的是為了避免每個單元測試類運行時都會重啟 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.     } 

接著在測試入口類中通過 @ComponentScan 對主入口啟動類 Application.class 及 RestClientConfig.class 進行排除。

 
 
 
 
  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.  

為了不單獨寫重復的代碼,我們一般會把單獨的代碼抽取出來作為一個公共基類,其中 @ExtendWith(SpringExtension.class) 注解目的是告訴 Spring boot 將使用 Junit5 作為運行平臺,如果想買中使用 Junit4 的話,則需要使用 @RunWith(SpringRunner.class) 注解告知用 SpringRunner 運行啟動。

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

準備好配置環(huán)境后,我們便可以開始對項目的 Mapper、Service、Web 層進行測試了。

Mapper層測試

對 Mapper 層的測試主要是驗證 SQL 語句及 Mybatis 傳參等準確性。

 
 
 
 
  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-commit: true 
  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 

對 Mapper 層的測試并沒有采取 Mock 的方式,而是采用 H2 內存數(shù)據(jù)庫的方式模擬真實數(shù)據(jù)庫,同時也避免由于測試數(shù)據(jù)給真實數(shù)據(jù)庫帶來的影響。

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

配置 H2 數(shù)據(jù)庫信息,同時 INIT 指定在創(chuàng)建連接時會執(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 demoList = demoMapper.getList(); 
  25.         Assert.assertNotNull(demoList); 
  26.         Assert.assertEquals(1, demoList.size()); 
  27.     } 

Service層測試

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

 
 
 
 
  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.       //測試第一個分支邏輯 
  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 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.       //測試第二個分支邏輯 
  31.       Mockito.when(redissonClient.getAtomicLong(ArgumentMatchers.anyString())).thenReturn(rAtomicLong); 
  32.       count = 1L; 
  33.       Mockito.when(rAtomicLong.incrementAndGet()).thenReturn(count); 
  34.  
  35.       MockedStatic 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.       //測試第三個分支邏輯 
  47.       Mockito.when(redissonClient.getAtomicLong(ArgumentMatchers.anyString())).thenReturn(rAtomicLong); 
  48.       count = 1L; 
  49.       Mockito.when(rAtomicLong.incrementAndGet()).thenReturn(count); 
  50.  
  51.       //執(zhí)行真實方法 
  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層測試

 
 
 
 
  1. public class DemoControllerTest extends SpringBaseTest { 
  2.  
  3.   private MockMvc mockMvc; 
  4.  
  5.   @Mock 
  6.   p
    網(wǎng)頁題目:外部依賴太多,如何寫 Java 單元測試?
    分享網(wǎng)址:http://www.dlmjj.cn/article/ccsgdco.html