为什么要写单元测试?如何写单元测试?

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 为什么要写单元测试?如何写单元测试?
  • 01、为什么要写单元测试
  • 02、到底如何写单元测试

01、为什么要写单元测试

一聊起测试用例,很多人第一反应就是,我们公司的测试会写测试用例的,我自己也会使用postman或者swagger之类的进行代码自测。那我们研发到底要不要写单元测试用例呢?参考阿里巴巴开发手册,第8条规则(单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%),大厂的要求就是必须喽。我个人感觉,写单元测试用例也是很有必要的,好处很多,例如:

  1. 保证代码质量!!!无论初级,中级,高级攻城狮开发工程的代码,且不说效率如何,功能是必要要保证是正确的;交付测试以后,bug锐减,联调飞快。
  2. 代码逻辑“文档化”!!!新人接手维护模块代码时,通过单元测试用例,以debug的方式就能熟悉业务代码。比起,看代码,研究表结构梳理代码结构,效率提升飞快。
  3. 易维护!!!新人接手维护代码模块时,提交自己的代码时,远行之前的单元测试达到回归测试,保证了新改动不会影响老业务。
  4. 快速定位bug!!!在联调期间,测试提出bug后,基于uat环境,编写出错的api测试用例。根据,测试提供的参数和token就可以以debug的方式跟踪问题的所在,如果是在微服务架构中,运行单元测试用例,不会注册本地服务到uat环境,还能过正常请求注册中心的服务。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

02、到底如何写单元测试

Java开发springboot项目都是基于junit测试框架,比较MockitoJUnitRunner与SpringRunner与使用,MockitoJUnitRunner基于mockito,模拟业务条件,验证代码逻辑。SpringRunner是MockitoJUnitRunner子类,集成了Spring容器,可以在测试的根据配置加载Spring bean对象。在Springboot开发中,结合@SpringBootTest注解,加载项目配置,进行单元测试。

基于MockitoJUnitRunner的方法测试

以springboot项目为例,一般,对单个的方法都是进行mock测试,在测试方法使用MockitoJUnitRunner,根据不同条件覆盖测试。使用@InjectMocks注解,可以让模拟的方法正常发起请求;@Mock注解可以模拟期望的条件。以删除菜单服务为例,源码如下:

@Service
public class MenuManagerImpl implements IMenuManager {
    /**
     * 删除菜单业务逻辑
     **/
    @Override
    @OptimisticRetry
    @Transactional(rollbackFor = Exception.class)
    public boolean delete(Long id) {
        if (Objects.isNull(id)) {
            return false;
        }
        Menu existingMenu = this.menuService.getById(id);
        if (Objects.isNull(existingMenu)) {
            return false;
        }
        if (!this.menuService.removeById(id)) {
            throw new OptimisticLockingFailureException("删除菜单失败!");
        }
        return true;
    }
}
 /**
  * 删除菜单方法级单元测试用例
  **/
@RunWith(MockitoJUnitRunner.class)
public class MenuManagerImplTest {
    @InjectMocks
    private MenuManagerImpl menuManager;
    @Mock
    private IMenuService menuService;
    @Test
    public void delete() {
        Long id = null;
        boolean flag;
        // id为空
        flag = menuManager.delete(id);
        Assert.assertFalse(flag);
        // 菜单返回为空
        id = 1l;
        Mockito.when(this.menuService.getById(ArgumentMatchers.anyLong())).thenReturn(null);
        flag = menuManager.delete(id);
        Assert.assertFalse(flag);
        // 修改成功
        Menu mockMenu = new Menu();
        Mockito.when(this.menuService.getById(ArgumentMatchers.anyLong())).thenReturn(mockMenu);
        Mockito.when(this.menuService.removeById(ArgumentMatchers.anyLong())).thenReturn(true);
        flag = menuManager.delete(id);
        Assert.assertTrue(flag);
    }
}

基于SpringRunner的Spring容器测试

在api开发过程中,会对单个api的调用链路进行验证,对第三方服务进行mock模拟,本服务的业务逻辑进行测试。一般,会使用@SpringBootTest加载测试环境的Spring容器配置,使用MockMvc以http请求的方式进行测试。以修改新增菜单测试用例为例,如下:

/**
 * 成功新增菜单api
*/
@Api(tags = "管理员菜单api")
@RestController
public class AdminMenuController {
    @Autowired
    private IMenuManager menuManager;
    @PreAuthorize("hasAnyAuthority('menu:add','admin')")
    @ApiOperation(value = "新增菜单")
    @PostMapping("/admin/menu/add")
    @VerifyLoginUser(type = IS_ADMIN, errorMsg = INVALID_ADMIN_TYPE)
    public Response<MenuVo> save(@Validated @RequestBody SaveMenuDto saveMenuDto) {
        return Response.success(menuManager.save(saveMenuDto));
    }
}
/**
 * 成功新增菜单单元测试用例
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MallSystemApplication.class)
@Slf4j
@AutoConfigureMockMvc
public class AdminMenuControllerTest extends BaseTest {
/**
 * 成功新增菜单
*/
@Test
public void success2save() throws Exception {
        SaveMenuDto saveMenuDto = new SaveMenuDto();
        saveMenuDto.setName("重置密码");
        saveMenuDto.setParentId(1355339254819966978l);
        saveMenuDto.setOrderNum(4);
        saveMenuDto.setType(MenuType.button.getValue());
        saveMenuDto.setVisible(MenuVisible.show.getValue());
        saveMenuDto.setUrl("https:baidu.com");
        saveMenuDto.setMethod(MenuMethod.put.getValue());
        saveMenuDto.setPerms("user:reset-pwd");
        // 发起http请求
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders
                .post("/admin/menu/add")
                .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .content(JSON.toJSONString(saveMenuDto))
                .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .header(GlobalConstant.AUTHORIZATION_HEADER, GlobalConstant.ADMIN_TOKEN))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
        Response<MenuVo> response = JSON.parseObject(mvcResult.getResponse().getContentAsString(), menuVoTypeReference);
        // 断言结果
        Assert.assertNotNull(response);
        MenuVo menuVo;
        Assert.assertNotNull(menuVo = response.getData());
        Assert.assertEquals(menuVo.getName(), saveMenuDto.getName());
        Assert.assertEquals(menuVo.getOrderNum(), saveMenuDto.getOrderNum());
        Assert.assertEquals(menuVo.getType(), saveMenuDto.getType());
        Assert.assertEquals(menuVo.getVisible(), saveMenuDto.getVisible());
        Assert.assertEquals(menuVo.getStatus(), MenuStatus.normal.getValue());
        Assert.assertEquals(menuVo.getUrl(), saveMenuDto.getUrl());
        Assert.assertEquals(menuVo.getPerms(), saveMenuDto.getPerms());
        Assert.assertEquals(menuVo.getMethod(), saveMenuDto.getMethod());
    }
}

具体编写单元测试用例规则参考测试用例的编写。简单说,一般api的单元测试用例,编写两类,如下:

  1. 业务参数的校验,和义务异常的校验。例如,名称是否为空,电话号码是否正确,用户未登陆则抛出未登陆异常。
  2. 各类业务场景的真实测试用例,例如,编写成功添加顶级菜单的测试用例,已经编写成功添加子级菜单的测试用例。

注意事项

  • 配置覆盖

此外,如上基于mockmvc的编写的测试用例,由于加载了Spring的配置,会对项目发起真实的调用。如果,环境的配置为线上配置,容易出现安全问题;一般,处于安全考虑,很多公司会对真实环境的修改操作做事务回滚操作,甚至根本就不会进行真实环境的调用,使用模拟环境替换,例如数据库的操作可以使用h2内存数据库进行替换。

这时,可以在src/test/resources目录下,添加与src/main/resources目录下,相同的文件进行配置覆盖 。src/test/main目录下的代码,会首先加载src/test/resources目录下的配置,如果没有则在加载src/main/resources目录的配置。常用场景如下:

  1. 在单元测试环境使用使用内存数据库。
  2. ginkens代码集成运行测试用例时,不希望在集成环境中输出日志文件信息,并且以debug级别输出日志。

以日志文件配置覆盖为例,在src/main/resources目录下配置日志有文件和控制台输出,如图:

微信图片_20220906165306.png

main/resource目录下的logback-spring.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">
 <contextName>mall-system</contextName>
    <!-- 控制台日志输出配置 -->
 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  <encoder>
   <pattern>
    [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level][%thread] [%contextName] [%logger{80}:%L] %msg%n
   </pattern>
   <charset>UTF-8</charset>
  </encoder>
  <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
   <level>DEBUG</level>
  </filter>
 </appender>
    <!-- 日志文件输出配置 -->
 <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <file>log/info.log</file>
  <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
   <fileNamePattern>log/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
   <maxFileSize>50MB</maxFileSize>
   <maxHistory>50</maxHistory>
   <totalSizeCap>10GB</totalSizeCap>
  </rollingPolicy>
  <encoder>
   <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level] [%contextName] [%logger{80}:%L] %msg%n</pattern>
  </encoder>
 </appender>
    <!-- 设置INFO 级别输出日志 -->
 <root level="INFO">
  <appender-ref ref="STDOUT" />
  <appender-ref ref="FILE" />
 </root>
</configuration>

src/test//resource目录下的新增logback-spring.xml,去掉日志文件输出的配置,设置日志输出级别为DEBUG;如果运行测试用例,则加载该配置不会进行日志文件的输出,并且打印DEBUG级别日志。如图:

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true">
    <contextName>mall-system</contextName>
    <!-- 控制台日志输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                [%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level][%thread] [%contextName] [%logger{80}:%L] %msg%n
            </pattern>
            <charset>UTF-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>DEBUG</level>
        </filter>
    </appender>
    <!-- DEBUG级别日志输出 -->
    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>
  • 指定环境

一般开发过程中,我们研发只会操作开发环境,也是为了避免数据安全问题,可以在单元测试用例中指定运行的环境配置。在测试类加上@ActiveProfiles("dev"),指定获取dev环境的配置。示例,

/**
* 获取dev环境配置
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MallSystemApplication.class)
@Slf4j
@AutoConfigureMockMvc
@ActiveProfiles("dev")
public class AdminMenuControllerTest extends BaseTest {
}

在联调测试中,对于出错的api,可以编写对应的单元测试用例,使用@ActiveProfiles("uat")指定到测试环境,就可以根据测试提供的参数快速定位问题。示例:

/**
 * 新增菜单api联调
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MallSystemApplication.class)
@Slf4j
@AutoConfigureMockMvc
@ActiveProfiles("uat")
public class AdminMenuControllerTest extends BaseTest {
/**
 * 成功新增菜单
*/
@Test
public void success2save() throws Exception {
String token="Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjhjMjhlZWEzLTA5MWEtNDA1OS1iMzliLTRjOGMyNGY4ZjEzMiJ9.xK9srWjeGaq4NXt4BzG2MQ_yN9IaYtPVjKj5MoSS4bX9Ytf1XJNe_NSupR0IItkB48G6mXVZwj5CIwWIYzvsEA";
    String paramJson="{
        "name":"mayuan",
        "parentId":"1",
        "orderNum":"1",
        "type":"1",
        "visible":true,
        "url":"https:baidu.com",
        "method":2,
        "perms":"user:reset-pwd"
   }";
   // 发起http请求
   MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders
                .post("/admin/menu/add")
                .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .content(paramJson)
                .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .header(GlobalConstant.AUTHORIZATION_HEADER, token))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
    }
}
相关文章
|
11天前
|
测试技术 开发者 UED
探索软件测试的深度:从单元测试到自动化测试
【10月更文挑战第30天】在软件开发的世界中,测试是确保产品质量和用户满意度的关键步骤。本文将深入探讨软件测试的不同层次,从基本的单元测试到复杂的自动化测试,揭示它们如何共同构建一个坚实的质量保证体系。我们将通过实际代码示例,展示如何在开发过程中实施有效的测试策略,以确保软件的稳定性和可靠性。无论你是新手还是经验丰富的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
3月前
|
JSON Dubbo 测试技术
单元测试问题之增加JCode5插件生成的测试代码的可信度如何解决
单元测试问题之增加JCode5插件生成的测试代码的可信度如何解决
55 2
单元测试问题之增加JCode5插件生成的测试代码的可信度如何解决
|
2月前
|
IDE 测试技术 持续交付
Python自动化测试与单元测试框架:提升代码质量与效率
【9月更文挑战第3天】随着软件行业的迅速发展,代码质量和开发效率变得至关重要。本文探讨了Python在自动化及单元测试中的应用,介绍了Selenium、Appium、pytest等自动化测试框架,以及Python标准库中的unittest单元测试框架。通过详细阐述各框架的特点与使用方法,本文旨在帮助开发者掌握编写高效测试用例的技巧,提升代码质量与开发效率。同时,文章还提出了制定测试计划、持续集成与测试等实践建议,助力项目成功。
84 5
|
3月前
|
JSON 测试技术 数据格式
单元测试问题之使用JCode5插件生成测试类如何解决
单元测试问题之使用JCode5插件生成测试类如何解决
125 3
|
3月前
|
测试技术
单元测试问题之使用TestMe时利用JUnit 5的参数化测试特性如何解决
单元测试问题之使用TestMe时利用JUnit 5的参数化测试特性如何解决
46 2
|
3月前
|
测试技术 C# 开发者
“代码守护者:详解WPF开发中的单元测试策略与实践——从选择测试框架到编写模拟对象,全方位保障你的应用程序质量”
【8月更文挑战第31天】单元测试是确保软件质量的关键实践,尤其在复杂的WPF应用中更为重要。通过为每个小模块编写独立测试用例,可以验证代码的功能正确性并在早期发现错误。本文将介绍如何在WPF项目中引入单元测试,并通过具体示例演示其实施过程。首先选择合适的测试框架如NUnit或xUnit.net,并利用Moq模拟框架隔离外部依赖。接着,通过一个简单的WPF应用程序示例,展示如何模拟`IUserRepository`接口并验证`MainViewModel`加载用户数据的正确性。这有助于确保代码质量和未来的重构与扩展。
75 0
|
3月前
|
测试技术 Java Spring
Spring 框架中的测试之道:揭秘单元测试与集成测试的双重保障,你的应用真的安全了吗?
【8月更文挑战第31天】本文以问答形式深入探讨了Spring框架中的测试策略,包括单元测试与集成测试的有效编写方法,及其对提升代码质量和可靠性的重要性。通过具体示例,展示了如何使用`@MockBean`、`@SpringBootTest`等注解来进行服务和控制器的测试,同时介绍了Spring Boot提供的测试工具,如`@DataJpaTest`,以简化数据库测试流程。合理运用这些测试策略和工具,将助力开发者构建更为稳健的软件系统。
54 0
|
3月前
|
测试技术 Java
全面保障Struts 2应用质量:掌握单元测试与集成测试的关键策略
【8月更文挑战第31天】Struts 2 的测试策略结合了单元测试与集成测试。单元测试聚焦于单个组件(如 Action 类)的功能验证,常用 Mockito 模拟依赖项;集成测试则关注组件间的交互,利用 Cactus 等框架确保框架拦截器和 Action 映射等按预期工作。通过确保高测试覆盖率并定期更新测试用例,可以提升应用的整体稳定性和质量。
73 0
|
3月前
|
测试技术 数据库
探索JSF单元测试秘籍!如何让您的应用更稳固、更高效?揭秘成功背后的测试之道!
【8月更文挑战第31天】在 JavaServer Faces(JSF)应用开发中,确保代码质量和可维护性至关重要。本文详细介绍了如何通过单元测试实现这一目标。首先,阐述了单元测试的重要性及其对应用稳定性的影响;其次,提出了提高 JSF 应用可测试性的设计建议,如避免直接访问外部资源和使用依赖注入;最后,通过一个具体的 `UserBean` 示例,展示了如何利用 JUnit 和 Mockito 框架编写有效的单元测试。通过这些方法,不仅能够确保代码质量,还能提高开发效率和降低维护成本。
50 0
|
3月前
|
Java 测试技术 API
SpringBoot单元测试快速写法问题之复杂的业务逻辑设计有效的单元测试如何解决
SpringBoot单元测试快速写法问题之复杂的业务逻辑设计有效的单元测试如何解决