【单元测试】SpringBoot
【单元测试】SpringBoot
1. 为什么单元测试很重要?‼️
从前,有一个名叫小明的程序员,他非常聪明,但有一个致命的缺点:懒惰。小明的代码写得又快又好,但他总觉得单元测试是一件麻烦事,觉得代码能跑就行,测试什么的全是浪费时间。
有一天,小明接到了一个重要的项目,他需要为一个在线购物网站开发一个新功能:用户可以在结账时使用优惠券。小明想:“这还不简单?半小时搞定!”于是,他迅速写好了代码,迫不及待地提交了。
第二天,项目经理来了,满脸怒气地对小明说:“小明,你的代码出问题了!所有用户在使用优惠券时都得到了负数的折扣,他们的账户反而被扣了更多的钱!”
小明惊讶地张大了嘴巴,不敢相信自己会犯这么低级的错误。他连忙检查代码,发现确实在计算折扣时,忘记处理负数的情况。小明赶紧修复了这个错误,但心里还是觉得不服气:“这只是个小问题,我不需要写单元测试。”
几天后,小明又收到一个新任务:实现一个积分系统,用户每消费一元就能积一分。小明想:“这次我一定不会犯错。”于是,他又快速写好了代码,提交了上去。
然而,不久之后,客户打电话过来抱怨:“我的积分怎么越消费越少了?!”
小明再次检查代码,发现自己在积分计算的函数里不小心多写了一个减号,导致积分被扣除而不是增加。他这次终于意识到,如果自己早点写单元测试,这些问题完全可以在开发阶段就被发现,而不是在上线后被用户发现。
于是,小明决定改过自新,认真学习单元测试。他发现,单元测试不仅可以帮助他捕捉到代码中的错误,还能让他更加自信地进行代码重构和优化。
总结
-
🔍早期发现问题:单元测试能够在开发阶段及时发现代码中的错误,避免错误在后期被发现,减少修复成本。
-
🐛确保代码质量:通过编写单元测试,可以验证每个模块的功能是否按预期工作,提升代码的可靠性和稳定性。
-
🔨方便重构:在进行代码重构或优化时,有单元测试作为保障,可以放心地修改代码,而不必担心引入新的错误
-
📄文档作用:单元测试可以作为代码的活文档,帮助新成员快速理解代码的功能和使用方法
2. 快速入门
2.1 基础配置
在 pom.xml 中添加以下依赖:
org.springframework.boot spring-boot-starter-test test
这个依赖包含了多个库和功能,主要有以下几个:
- JUnit:JUnit是Java中最流行和最常用的单元测试框架,它提供了一套注解和断言来编写和运行单元测试。例如@Test注解表示一个测试方法,assertEquals断言表示两个值是否相等。
- Spring Test:Spring Test是一个基于Spring的测试框架,它提供了一套注解和工具来配置和管理Spring上下文和Bean。例如@SpringBootTest注解表示一个集成测试类,@Autowired注解表示自动注入一个Bean。
- Mockito:Mockito是一个Java中最流行和最强大的Mock对象库,它可以模拟复杂的真实对象行为,从而简化测试过程。例如@MockBean注解表示创建一个Mock对象,when方法表示定义Mock对象的行为。
- Hamcrest:Hamcrest是一个Java中的匹配器库,它提供了一套语义丰富而易读的匹配器来进行结果验证。例如assertThat断言表示验证一个值是否满足一个匹配器,is匹配器表示两个值是否相等。
- AssertJ:AssertJ是一个Java中的断言库,它提供了一套流畅而直观的断言语法来进行结果验证。例如assertThat断言表示验证一个值是否满足一个条件,isEqualTo断言表示两个值是否相等。
除了以上这些库外,spring-boot-starter-test还包含了其他一些库和功能,如JsonPath、JsonAssert、XmlUnit等。这些库和功能可以根据不同的测试场景进行选择和使用。
Mockito详解地址:https://pdai.tech/md/develop/ut/dev-ut-x-mockito.html
2.2 编写单元测试
为了更好的演示如何编写单元测试,以最简单的用户登录为例🌰
项目结构
src ├── main │ └── java │ └── com │ └── hwq │ └── fuwork01 │ ├── common │ ├── controller │ │ └── UserController.java │ ├── dto │ ├── exception │ └── service │ └── UserService └── test └── java └── com └── hwq └── fuwork01 ├── controller │ └── UserControllerTest.java └── service └──FuWork01ApplicationTests.java
UserServiceImpl(Service层)
/** * @author wqh * @description 针对表【user(用户表)】的数据库操作Service实现 * @createDate 2024-07-15 17:13:27 */ @Service public class UserServiceImpl extends ServiceImpl implements UserService{ @Override public Long userLogin(String userAccount, String userPassword, HttpServletRequest request) { LambdaQueryWrapper userLambdaQueryWrapper = new LambdaQueryWrapper(); userLambdaQueryWrapper.eq(User::getUserAccount, userAccount) .eq(User::getUserPassword, userPassword); User user = this.getOne(userLambdaQueryWrapper); if (user == null) { throw new BusinessException(ErrorCode.NOT_FOUND_ERROR ,"用户不存在"); } // 存储用户登录态 request.getSession().setAttribute("userLogin", user); return user.getId(); } @Override public User getLoginUser(HttpServletRequest request) { return (User)request.getSession().getAttribute("userLogin"); } }
针对Service层的测试
@SpringBootTest public class UserServiceTest { @Resource private UserService userService; private HttpServletRequest request; @BeforeEach void setUp() { // 模拟构造request request = new MockHttpServletRequest(); } /** * 测试用户登录 */ @Test void userLogin() { String userAccount = "huang"; String userPassword = "huangwenqing"; Long userId = userService.userLogin(userAccount, userPassword, request); // 验证结果对象与user对象相等 assertThat(userId, Matchers.is(1L)); } }
解释
- request = new MockHttpServletRequest(),构造一个模拟的request
- assertThat,判断userId是否符合正常
新断言assertThat使用
JUnit 4.4 结合 Hamcrest 提供了一个全新的断言语法——assertThat。程序员可以只使用 assertThat 一个断言语句,结合 Hamcrest 提供的匹配符,就可以表达全部的测试思想。
assertThat 的优点:
优点 1: 以前 JUnit 提供了很多的 assertion 语句,如:assertEquals,assertNotSame,assertFalse,assertTrue,assertNotNull,assertNull 等,现在有了 JUnit 4.4,一条 assertThat 即可以替代所有的 assertion 语句,这样可以在所有的单元测试中只使用一个断言方法,使得编写测试用例变得简单,代码风格变得统一,测试代码也更容易维护。
优点 2: assertThat 使用了 Hamcrest 的 Matcher 匹配符,用户可以使用匹配符规定的匹配准则精确的指定一些想设定满足的条件,具有很强的易读性,而且使用起来更加灵活。
优点 3: assertThat 不再像 assertEquals 那样,使用比较难懂的“谓宾主”语法模式(如:assertEquals(3, x);),相反,assertThat 使用了类似于“主谓宾”的易读语法模式(如:assertThat(x,is(3));),使得代码更加直观、易读。
UserController(登录控制层)
@RestController @RequestMapping("/user") @CrossOrigin("*") public class UserController { @Resource private UserService userService; @PostMapping("/login") public BaseResponse userLogin(@RequestBody UserLoginDTO userLoginDTO, HttpServletRequest request) { if (userLoginDTO == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数错误"); } String userAccount = userLoginDTO.getUserAccount(); String userPassword = userLoginDTO.getUserPassword(); if (StringUtils.isEmpty(userAccount)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "账户不得为空"); } if (StringUtils.isEmpty(userPassword)) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码不得为空"); } return ResultUtils.success(userService.userLogin(userAccount, userPassword, request)); } }
内容
- 对上传的登录参数进行校验
- 登录成功,返回用户id
针对controller层单元测试
@SpringBootTest @AutoConfigureMockMvc public class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserService userService; /** * 用户登录成功测试 * @throws Exception */ @Test void testUserLoginSuccess() throws Exception { UserLoginDTO userLoginDTO = new UserLoginDTO(); userLoginDTO.setUserAccount("huang"); userLoginDTO.setUserPassword("huangwenqing"); // userService测试 when(userService.userLogin("huang", "huangwenqing", new MockHttpServletRequest())).thenReturn(1L); // 模拟http登录请求 mockMvc.perform(post("/user/login") .contentType(MediaType.APPLICATION_JSON) .content("{\"userAccount\":\"huang\",\"userPassword\":\"huangwenqing\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(0)) .andExpect(jsonPath("$.data").value(0L)); } /** * 用户登录异常测试 * @throws Exception */ @Test void testUserLoginNullParams() throws Exception { mockMvc.perform(post("/user/login") .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(40000)); } }
要点
- @SpringBootTest: 加载完整的 Spring 应用程序上下文。
- @AutoConfigureMockMvc: 自动配置 MockMvc,用于模拟 HTTP 请求。
- MockMvc: 用于模拟 HTTP 请求和响应的测试。
- @MockBean: 创建并注入一个模拟的 UserService 实例,以便于测试控制器而不需要实际的服务实现。
- 使用 mockMvc.perform 方法发送 POST 请求,模拟用户登录。
- 使用 andExpect 方法验证 HTTP 状态码和响应体中的数据。
2.3测试原则
- 保持测试独立:确保每个测试独立运行,不依赖其他测试的执行结果
- 使用模拟对象:对于外部依赖,如数据库、网络请求等,尽量使用模拟对象,以提高测试的速度和稳定性。
- 覆盖各种场景:编写充分的测试用例,覆盖正常路径、异常路径和边界条件。
- 保持测试简洁:测试代码应该简洁明了,避免过于复杂的逻辑,以提高可维护性。
3.总结
编写优雅的单元测试是保证代码质量的关键。在 Spring Boot 中,我们可以使用 @SpringBootTest 和 @AutoConfigureMockMvc 等注解简化测试配置,使用 Mockito 等工具模拟依赖,编写覆盖全面的测试用例。通过遵循最佳实践,我们可以编写高效、稳定的单元测试,提高开发效率和代码质量。
希望本文对您在 Spring Boot 项目中编写单元测试有所帮助。Happy Testing!