文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

外部依赖太多,如何写 Java 单元测试?

2024-12-03 03:12

关注

事出有因

在日常的开发中,很多人习惯性地写完需求代码后,嗖的一声用 Postman 模拟真实请求或写几个 JUnit 的单元测试跑功能点,只要没有问题就上线了,但其实这存在很大风险,一方面无法验证业务逻辑的不同分支,另外一方面需严重依赖中间件资源才能运行测试用例,占用大量资源。

秣马厉兵

Mockito是一个非常优秀的模拟框架,可以使用它简洁的API来编写漂亮的测试代码,它的测试代码可读性高同时会产生清晰的错误日志。

添加 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,同时在执行测试用例前初始化 MockitoAnnotations.initMocks(this) 告诉框架使 Mock 相关注解生效。

验证对象行为 Verify

  1. @Test 
  2. public void testVerify(){ 
  3.     //创建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()参数匹配器,并存根 
  12.     when(mockedList.get(anyInt())).thenReturn("element"); 
  13.     System.out.println(mockedList.get(2)); //此处输出为element 
  14.     verify(mockedList).get(anyInt()); 

存根 stubbing

stubbing 完全是模拟一个外部依赖、用来提供测试时所需要的数据。

  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(); 

存根的连续调用

  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.     //任何连续调用: 还是打印 "foo" (最后的存根生效). 
  10.     System.out.println(mock.someMethod("some arg")); 
  11.     //可供选择的连续存根的更短版本: 
  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")); 

在做方法存根时,可以指定不同时机需要提供的测试数据,例如第一次调用返回 xxx,第二次调用时抛出异常等。

参数匹配器

  1. @Test 
  2. public void testArugument{ 
  3.     //使用内建anyInt()参数匹配器 
  4.     when(mockedList.get(anyInt())).thenReturn("element"); 
  5.     System.out.println(mockedList.get(999)); //打印 "element" 
  6.     //同样可以用参数匹配器做验证 
  7.     verify(mockedList).get(anyInt()); 
  8.  
  9.     //注意:如果使用参数匹配器,所有的参数都必须通过匹配器提供。 
  10.     verify(mock) 
  11.     .someMethod(anyInt(), anyString(), eq("third argument")); 
  12.     //上面是正确的 - eq(0也是参数匹配器),而下面的是错误的 
  13.     verify(mock) 
  14.     .someMethod(anyInt(), anyString(), "third argument"); 

验证调用次数

  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创建 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 类或者接口,对于静态、私有及final方法的 Mock 则无能为力了。

而 PowerMock 正是弥补这块的缺陷,它的实现原理如下:

但值得高兴的是在 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 配置

  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.     -- spring boot相关依赖 --> 
  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.     -- Mockito --> 
  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.     -- 由于mockito-core自带的byte-buddy版本低,无法使用mock静态方法 --> 
  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.     -- mybatis --> 
  78.      
  79.         org.mybatis.spring.boot 
  80.         mybatis-spring-boot-starter 
  81.         ${mybatis-spring.version} 
  82.      
  83.  
  84.     -- redisson --> 
  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.     -- mysql --> 
  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.     -- junit5 --> 
  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.     -- H2数据库--> 
  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.                 --指定在mvn的test阶段执行此插件 --> 
  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.                 -- attached to Maven test phase --> 
  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.                         -- select non-aggregate reports --> 
  210.                         report 
  211.                      
  212.                  
  213.              
  214.          
  215.      
  216.  

 

 

 

 

 

 

maven 运行测试用例是通过调用 maven 的 surefire 插件并 fork 一个子进程来执行用例的。

forkMode 属性指明是为每个测试创建一个进程还是所有测试共享同一个进程完成,forkMode 设置值有 never、once、always 、pertest 。

环境准备

在项目中 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 {} 

准备好配置环境后,我们便可以开始对项目的 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-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:mapperSuiteTest.java 
  29.          
  30.      
  31.  

 

 

 

 

生成覆盖率报告

  1.  
  2.     org.jacoco 
  3.     jacoco-maven-plugin 
  4.     ${jacoco.version} 
  5.      
  6.          
  7.              
  8.                 prepare-agent 
  9.              
  10.          
  11.         -- 绑定到test阶段 --> 
  12.          
  13.             report 
  14.             test 
  15.              
  16.                 report 
  17.              
  18.          
  19.      
  20.  

项目中使用 jacoco 作为代码覆盖率工具,在命令行中运行 mvn clean test 后会执行所有单元测试用例,随后会在 target 目录下生成site 文件夹,文件夹包含 jacoco 插件生成的测试报告。

报告中主要包含本次测试中涉及到的类、方法、分支覆盖率,其中红色的表示未被覆盖到,绿色表示全覆盖,黄色的则表示部分覆盖到,可点击某个包或某个类查看具体哪些行未被覆盖等。

注意:

言而总之

虽然编写单元测试会带来一定的工作量,但通过使用 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

 

来源:码农私房话内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯