如何编写单元测试代码

Junit5+Mockito+jacoco+h2本地数据库 Idea增强插件

  • JUnitGenerator V2.​0 用于生成测试用例的标准模块
  • GenerateAllSet 用于快速new创建对象,并设置默认值
  • MybatisX dao与mapper的关联映射 方便查看
  1. ########################################################################################
  2. ##
  3. ## Available variables:
  4. ## $entryList.methodList - List of method composites
  5. ## $entryList.privateMethodList - List of private method composites
  6. ## $entryList.fieldList - ArrayList of class scope field names
  7. ## $entryList.className - class name
  8. ## $entryList.packageName - package name
  9. ## $today - Todays date in MM/dd/yyyy format
  10. ##
  11. ## MethodComposite variables:
  12. ## $method.name - Method Name
  13. ## $method.signature - Full method signature in String form
  14. ## $method.reflectionCode - list of strings representing commented out reflection code to access method (Private Methods)
  15. ## $method.paramNames - List of Strings representing the method's parameters' names
  16. ## $method.paramClasses - List of Strings representing the method's parameters' classes
  17. ##
  18. ## You can configure the output class name using "testClass" variable below.
  19. ## Here are some examples:
  20. ## Test${entry.ClassName} - will produce TestSomeClass
  21. ## ${entry.className}Test - will produce SomeClassTest
  22. ##
  23. ########################################################################################
  24. ##
  25. ## 首字母大写
  26. #macro (cap $strIn)$strIn.valueOf($strIn.charAt(0)).toUpperCase()$strIn.substring(1)#end
  27. ## 首字母小写 自定义down
  28. #macro (down $strIn)$strIn.valueOf($strIn.charAt(0)).toLowerCase()$strIn.substring(1)#end
  29. ## Iterate through the list and generate testcase for every entry.
  30. #foreach ($entry in $entryList)
  31. #set( $testClass="${entry.className}Test")
  32. ##
  33. /*
  34. * Licensed to the Apache Software Foundation (ASF) under one or more
  35. * contributor license agreements. See the NOTICE file distributed with
  36. * this work for additional information regarding copyright ownership.
  37. * The ASF licenses this file to You under the Apache License, Version 2.0
  38. * (the "License"); you may not use this file except in compliance with
  39. * the License. You may obtain a copy of the License at
  40. *
  41. * http://www.apache.org/licenses/LICENSE-2.0
  42. *
  43. * Unless required by applicable law or agreed to in writing, software
  44. * distributed under the License is distributed on an "AS IS" BASIS,
  45. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  46. * See the License for the specific language governing permissions and
  47. * limitations under the License.
  48. */
  49. package $entry.packageName;
  50. import org.junit.jupiter.api.AfterEach;
  51. import org.junit.jupiter.api.BeforeEach;
  52. import org.junit.jupiter.api.DisplayName;
  53. import org.junit.jupiter.api.Test;
  54. import org.springframework.beans.factory.annotation.Autowired;
  55. /**
  56. * ${entry.className} Tester
  57. */
  58. public class $testClass {
  59. @Autowired
  60. private ${entry.className} #down(${entry.className});
  61. @BeforeEach
  62. @DisplayName("Each unit test method is executed once before execution")
  63. public void before() throws Exception {
  64. }
  65. @AfterEach
  66. @DisplayName("Each unit test method is executed once before execution")
  67. public void after() throws Exception {
  68. }
  69. #foreach($method in $entry.methodList)
  70. @Test
  71. @DisplayName("Method description: ...")
  72. public void test#cap(${method.name})() throws Exception {
  73. //TODO: Test goes here...
  74. }
  75. #end
  76. #foreach($method in $entry.privateMethodList)
  77. @Test
  78. @DisplayName("Method description: ...")
  79. public void test#cap(${method.name})() throws Exception {
  80. //TODO: Test goes here...
  81. #foreach($string in $method.reflectionCode)
  82. $string
  83. #end
  84. }
  85. #end
  86. }
  87. #end

test-0

1.配置配置测试类生成路径
原配置:${SOURCEPATH}/test/${PACKAGE}/${FILENAME} 修改后配置:${SOURCEPATH}/../../test/java/${PACKAGE}/${FILENAME} 如图: test-1 2.选择类——>右键——>Generate——>Junit Test,生成测试类
test-2

  • 1.单元测试代码目录 必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。
    说明:源码编译时会跳过此目录,而单元测试框架默认是扫描此目录,测试的配置文件必须放在:src/test/resources文件下

  • 2.测试类所在的包名应该和被测试类所在的包名保持一致
    示例:
    业务类 src/main/java/org/apache/linkis/jobhistory/dao/JobDetailMapper.java
    对应的测试类 src/test/java/org/apache/linkis/jobhistory/dao/JobDetailMapperTest.java

  • 3.测试类的命名定义规范:使用Test作为类名的后缀
    测试类的命名如下:
    被测试的业务+Test、被测试的接口+Test、被测试的类+Test

  • 4.测试用例的命名定义规范:使用test作为方法名的前缀
    测试用例的命名规则是:test+方法名。避免使用test1、test2没有含义的名称,其次需要有必要的函数方法注释。

  • 1.单元测试中不准使用 System.out 来进行人肉验证,或则if判断来验证(可以使用log进行关键日志输出),必须使用断言 assert 来验证。

  • 2.保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。
    反例:method2 需要依赖 method1 的执行,将执行结果作为 method2 的输入

  • 3.单元测试必须可以重复执行的,不能受到外界环境的影响。 说明:单元测试通常会被放到持续集成中,每次有代码 check in 时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
    正例:为了不受外界环境影响,要求设计代码时就把被测类的相关依赖改成注入,在测试时用 spring 这样的依赖注入框架注入一个本地(内存)实现或者 Mock 实现。

  • 4.增量代码确保单元测试通过。
    说明:新增代码必须补充单元测试,如果新增代码影响了原有单元测试,请修正

  • 5.对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度一般都是方法级别(工具类或则枚举类等极少场景可以是类级别)。
    说明:只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域。

所有的测试用例的结果验证都必须使用断言模式 优先使用Junit5的Assertions断言,极少数场景允许使用AssertJ的断言

方法说明备注
assertEquals判断两个对象或两个原始类型是否相等
assertNotEquals判断两个对象或两个原始类型是否不相等
assertTrue判断给定的布尔值是否为 true
assertFalse判断给定的布尔值是否为 false
assertNull判断给定的对象引用是否为 null
assertNotNull判断给定的对象引用是否不为 null
assertAll将多个判断逻辑放在一起处理,只要有一个报错就会导致整体测试不通过

组合断言 assertAll方法可以将多个判断逻辑放在一起处理,只要有一个报错就会导致整体测试不通过:

  1. @Test
  2. @DisplayName("assert all")
  3. public void all() {
  4. //将多个判断放在一起执行,只有全部通过才算通过
  5. assertAll("Math",
  6. () -> assertEquals(2, 1 + 1),
  7. () -> assertTrue(1 > 0)
  8. );
  9. }

异常断言 Assertions.assertThrows方法,用来测试Executable实例执行execute方法时是否抛出指定类型的异常; 如果execute方法执行时不抛出异常,或者抛出的异常与期望类型不一致,都会导致测试失败; 示例:

  1. @Test
  2. @DisplayName("异常的断言")
  3. void exceptionTesting() {
  4. // 其execute方法执行时,如果抛出了异常,并且异常的类型是assertThrows的第一个参数(这里是ArithmeticException.class),
  5. // 返回值是异常的实例
  6. Exception exception = assertThrows(ArithmeticException.class, () -> Math.floorDiv(1,0));
  7. log.info("assertThrows通过后,返回的异常实例:{}", exception.getMessage());
  8. }

对象实例是否相等断言
1.是否是同一个对象实例

  1. 使用JunitdAssertions.assertEquals
  2. Assertions.assertEquals(expectedJobDetail, actualJobDetail)

不是同一个实例,但是比较实例的属性值是否完全相等 AssertJ

  1. 常用场景 数据库更新操作前/后的对象比较
  2. 使用AssertJassertThat断言usingRecursiveComparison模式
  3. Assertions.assertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);

2.list等集合结果的断言 结果集集合的大小需要断言 范围或则具体大size

结果集集合中的每个对象需要断言,推荐结合stream模式的Predicate进行使用 示例:

  1. ArrayList<JobRespProtocol> jobRespProtocolArrayList=service.batchChange(jobDetailReqBatchUpdate);
  2. //list配和stream的predicate进行断言判断
  3. Predicate<JobRespProtocol> statusPrecate = e -> e.getStatus()==0;
  4. assertEquals(2, jobRespProtocolArrayList.size());
  5. assertTrue(jobRespProtocolArrayList.stream().anyMatch(statusPrecate));

有时我们单测一些api或者service模块,其中的service或者dao对于一些方法的返回值默认是null,但是逻辑里面有对这个返回null的对象进行判断或者二次取值的话,就是引发一些异常

示例:

  1. PageInfo<UDFAddVo> pageInfo =
  2. udfService.getManagerPages(udfName, udfTypes, userName, curPage, pageSize);
  3. message = Message.ok();
  4. // 这里的pageInfo是null,后续的get方法就会出现异常
  5. message.data("infoList", pageInfo.getList());
  6. message.data("totalPage", pageInfo.getPages());
  7. message.data("total", pageInfo.getTotal());

mock模拟数据示例:

  1. PageInfo<UDFAddVo> pageInfo = new PageInfo<>();
  2. pageInfo.setList(new ArrayList<>());
  3. pageInfo.setPages(10);
  4. pageInfo.setTotal(100);
  5. // 对 udfService.getManagerPages 方法进行任意传递参数,模拟返回pageInfo对象
  6. // 有了这个模拟数据,上面示例在执行get方法的时候,就不会有异常
  7. Mockito.when(
  8. udfService.getManagerPages(
  9. Mockito.anyString(),
  10. Mockito.anyCollection(),
  11. Mockito.anyString(),
  12. Mockito.anyInt(),
  13. Mockito.anyInt()))
  14. .thenReturn(pageInfo);

按类的大功能可以大体分类

  • Controller 提供http服务的controller 配合mockmvc做单元测试
  • Service 业务逻辑代码的service层
  • Dao 与数据库操作的Dao层
  • util工具功能类 常用的功能工具
  • exception类 自定义的异常类
  • enum类 枚举类
  • entity类 用于DB交互以及方法处理的参数VO对象等实体类(若除了正常的get set外还有其他自定义函数的需要进行单元测试)

使用Mockmvc 主要验证 接口请求RequestMethod方式,基本参数,以及返回结果预期。 主要场景:带上非必要参数和不带非必要参数的场景 异常

  1. @Test
  2. public void testList() throws Exception {
  3. //带上非必要参数
  4. MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>();
  5. paramsMap.add("startDate", String.valueOf(System.currentTimeMillis()));
  6. MvcResult mvcResult = mockMvc.perform(get("/jobhistory/list")
  7. .params(paramsMap))
  8. .andExpect(status().isOk())
  9. .andExpect(content().contentType(MediaType.APPLICATION_JSON))
  10. .andReturn();
  11. Message res = JsonUtils.jackson().readValue(mvcResult.getResponse().getContentAsString(), Message.class);
  12. assertEquals(res.getStatus(), MessageStatus.SUCCESS());
  13. logger.info(mvcResult.getResponse().getContentAsString());
  14. //不带非必要参数
  15. mvcResult = mockMvc.perform(get("/jobhistory/list"))
  16. .andExpect(status().isOk())
  17. .andExpect(content().contentType(MediaType.APPLICATION_JSON))
  18. .andReturn();
  19. res = JsonUtils.jackson().readValue(mvcResult.getResponse().getContentAsString(), Message.class);
  20. assertEquals(res.getStatus(), MessageStatus.SUCCESS());
  21. logger.info(mvcResult.getResponse().getContentAsString());
  22. }
  1. //todo

使用H2数据库,配置文件中application.properties中需要配置H2数据库的基本信息,以及mybatis的相关路径信息

  1. #h2数据库配置
  2. spring.datasource.driver-class-name=org.h2.Driver
  3. #连接数据库
  4. spring.datasource.url=jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=true
  5. #初始化数据库表的脚本
  6. spring.datasource.schema=classpath:create.sql
  7. #初始化数据库表中的数据的脚本
  8. spring.datasource.data=classpath:data.sql
  9. spring.datasource.username=sa
  10. spring.datasource.password=
  11. spring.datasource.hikari.connection-test-query=select 1
  12. spring.datasource.hikari.minimum-idle=5
  13. spring.datasource.hikari.auto-commit=true
  14. spring.datasource.hikari.validation-timeout=3000
  15. spring.datasource.hikari.pool-name=linkis-test
  16. spring.datasource.hikari.maximum-pool-size=50
  17. spring.datasource.hikari.connection-timeout=30000
  18. spring.datasource.hikari.idle-timeout=600000
  19. spring.datasource.hikari.leak-detection-threshold=0
  20. spring.datasource.hikari.initialization-fail-timeout=1
  21. #配置mybatis-plus的mapper信息 因为使用的是mybatis-plus,使用mybatis-plus浅醉
  22. mybatis-plus.mapper-locations=classpath:org/apache/linkis/jobhistory/dao/impl/JobDetailMapper.xml,classpath:org/apache/linkis/jobhistory/dao/impl/JobHistoryMapper.xml
  23. mybatis-plus.type-aliases-package=org.apache.linkis.jobhistory.entity
  24. mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

编写规范

  1. 使用@Transactional以及@Rollback 实现数据回滚,避免数据污染
  2. 每一个DaoTest应该有一个创建初始化数据公共方法(或导入数据的方式csv)来准备数据,相关的查询,更新,删除等操作都应该先调用该公共方法进行数据的准备
  3. 创建测试的数据,如果某属性值是自增id,则不应该进行赋值
  4. 创建的测试数据,应尽可能和实际样例数据保持一致
  5. 更新数据测试时,如果字段允许,请带上modify-原始值前缀