从前,有一个名叫小明的程序员,他非常聪明,但有一个致命的缺点:懒惰。小明的代码写得又快又好,但他总觉得单元测试是一件麻烦事,觉得代码能跑就行,测试什么的全是浪费时间。
有一天,小明接到了一个重要的项目,他需要为一个在线购物网站开发一个新功能:用户可以在结账时使用优惠券。小明想:“这还不简单?半小时搞定!”于是,他迅速写好了代码,迫不及待地提交了。
第二天,项目经理来了,满脸怒气地对小明说:“小明,你的代码出问题了!所有用户在使用优惠券时都得到了负数的折扣,他们的账户反而被扣了更多的钱!”
小明惊讶地张大了嘴巴,不敢相信自己会犯这么低级的错误。他连忙检查代码,发现确实在计算折扣时,忘记处理负数的情况。小明赶紧修复了这个错误,但心里还是觉得不服气:“这只是个小问题,我不需要写单元测试。”
几天后,小明又收到一个新任务:实现一个积分系统,用户每消费一元就能积一分。小明想:“这次我一定不会犯错。”于是,他又快速写好了代码,提交了上去。
然而,不久之后,客户打电话过来抱怨:“我的积分怎么越消费越少了?!”
小明再次检查代码,发现自己在积分计算的函数里不小心多写了一个减号,导致积分被扣除而不是增加。他这次终于意识到,如果自己早点写单元测试,这些问题完全可以在开发阶段就被发现,而不是在上线后被用户发现。
于是,小明决定改过自新,认真学习单元测试。他发现,单元测试不仅可以帮助他捕捉到代码中的错误,还能让他更加自信地进行代码重构和优化。
🔍早期发现问题:单元测试能够在开发阶段及时发现代码中的错误,避免错误在后期被发现,减少修复成本。
🐛确保代码质量:通过编写单元测试,可以验证每个模块的功能是否按预期工作,提升代码的可靠性和稳定性。
🔨方便重构:在进行代码重构或优化时,有单元测试作为保障,可以放心地修改代码,而不必担心引入新的错误
📄文档作用:单元测试可以作为代码的活文档,帮助新成员快速理解代码的功能和使用方法
在 pom.xml 中添加以下依赖:
org.springframework.boot spring-boot-starter-test test
这个依赖包含了多个库和功能,主要有以下几个:
除了以上这些库外,spring-boot-starter-test还包含了其他一些库和功能,如JsonPath、JsonAssert、XmlUnit等。这些库和功能可以根据不同的测试场景进行选择和使用。
Mockito详解地址:https://pdai.tech/md/develop/ut/dev-ut-x-mockito.html
为了更好的演示如何编写单元测试,以最简单的用户登录为例🌰
项目结构
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
/** * @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"); } }
@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)); } }
解释
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));),使得代码更加直观、易读。
@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)); } }
内容
@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)); } }
要点
编写优雅的单元测试是保证代码质量的关键。在 Spring Boot 中,我们可以使用 @SpringBootTest 和 @AutoConfigureMockMvc 等注解简化测试配置,使用 Mockito 等工具模拟依赖,编写覆盖全面的测试用例。通过遵循最佳实践,我们可以编写高效、稳定的单元测试,提高开发效率和代码质量。
希望本文对您在 Spring Boot 项目中编写单元测试有所帮助。Happy Testing!