ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot单元测试实践:让代码更健壮的写法

SpringBoot单元测试实践:让代码更健壮的写法 一个没有单元测试的SpringBoot项目就像一座没有消防通道的大楼——平时住着确实舒服一旦发生火灾所有人都只能往窗户口挤。你可能写出了一个能跑起来的Controller也自信Service层的逻辑天衣无缝但每一次重构都像是在雷区里奔跑每一次升级依赖都提心吊胆。单元测试不是写在简历上的装饰品它是你唯一能亲手证明“我的代码没有爆炸”的证据。SpringBoot的生态给了我们近乎奢侈的测试工具但绝大多数开发者只使用了一个最肤浅的皮毛启动完整上下文、注入一个MockMvc、对着数据库做一次assert。这本质上是在用集成测试的昂贵代价掩盖单元测试的缺席。当你每次跑测试都要等待5秒启动Spring容器时你早就失去了写测试的欲望——这就是问题所在。你需要的是“切片”而不是整个蛋糕SpringBoot最伟大的设计之一是WebMvcTest和DataJpaTest这类切片测试注解。它们只加载你正在测试的那一层Bean而不是把整个ApplicationContext拖下水。一个Controller的测试只需要MockMvc和相关的Controller、Filter、ControllerAdvice至于Service和Repository全部用Mock替换。WebMvcTest(UserController.class) class UserControllerTest { Autowired private MockMvc mockMvc; MockBean private UserService userService; Test void shouldReturnUserWhenFound() throws Exception { when(userService.findByUsername(neo)).thenReturn(new User(neo, 99)); mockMvc.perform(get(/users/neo)) .andExpect(status().isOk()) .andExpect(jsonPath($.age).value(99)); } }这段测试的运行时间通常在几百毫秒以内。启动耗时超过1秒的单元测试都是在变相鼓励开发者删除它们。切片测试的价值不是快这么简单它强制你思考“这个类的契约是什么”而不是沉浸在整个系统的混沌联动中。Mock不是敌人滥用Mock才是很多人对Mock抱有一种矛盾心理觉得Mock出来的测试都是“假的”不如真实环境可信。但请记住一句冷峻的话单元测试测的是“我写的代码”不是“别人写的代码”。如果你在测试自己的Service时真的去调用了数据库那么测试失败时你根本分不清是自己逻辑错了还是Hibernate的懒加载策略坑了你。正确做法是把边界清晰地画出来Service的测试中Mock RepositoryController的测试中Mock Service。但要注意Mock对象要设定合理的返回值而不是一律when(...).thenReturn(null)。一个只会返回null的Mock测试出来的代码是只会防御null的代码。同时警惕verify()被滥用。verify(userRepository).save(user)确实证明了调用了save但如果你把过多的精力放在“方法被调用了”而不是“结果符合预期”测试就会退化成对实现细节的复读机。测试应该锁定行为而不是锁定调用次数。断言方式决定了你的测试是遗嘱还是资产JUnit自带的assertEquals(expected, actual)确实能用但读起来像在念遗嘱。当断言失败时只得到一句expected: true but was: false你还要自己翻代码定位到底哪个字段出了问题。AssertJ的出现彻底改变了这个局面它允许你写出流式、可读性极高的断言assertThat(user.getEmail()) .contains() .endsWith(.com) .isNotBlank(); assertThat(user.getRoles()) .hasSize(2) .contains(ADMIN, USER);失败信息会明确指出“Expected to contain: ADMIN but was missing among: [USER, GUEST]”。断言信息是测试给出的唯一诊断报告写不清楚断言就等于让后来的维护者蒙着眼睛做手术。不要在链式断言的末尾无脑加.isEqualTo()每一个断言都应基于业务规则而不是凑数。用ParameterizedTest把边界值一网打尽看过了太多用Test写五个重复测试的代码每个测试方法名不同但内部逻辑几乎一致。SpringBoot配合JUnit 5提供的参数化测试能让你把精力放在测试数据上而不是复制粘贴ParameterizedTest ValueSource(ints {0, -1, Integer.MIN_VALUE}) void shouldRejectNonPositiveAge(int age) { assertThrows(IllegalArgumentException.class, () - userService.validateAge(age)); }更高级的用法是CsvSource加载多组数据或者MethodSource提供复杂对象。一个业务的边界值如果少于五组说明你对这个业务的理解还不够深刻。空值、空字符串、超长字符串、特殊字符这些“脏数据”才是导致线上崩溃的真凶。参数化测试让这些脏数据有了一个体面且高效的归宿。数据库测试别在内存数据库上自欺欺人很多团队喜欢用H2替代MySQL跑测试因为启动快、无需安装。但H2的方言、锁行为、字符集处理与MySQL存在微妙差异经常出现测试全绿、上线崩盘的局面。用内存数据库跑单元测试得到的不是“测试成功”而是“测试在撒谎”。更稳妥的方案是使用Testcontainers在测试启动时拉取一个真实的MySQL或PostgreSQL容器。SpringBoot的DataJpaTest支持自动配置Testcontainers只需要一个Container注解和几行配置就能让测试运行在一个真实的数据库环境中。代价是多了几秒的容器启动时间但换来的是一杯咖啡时间就能发现方言问题的机会。Testcontainers DataJpaTest class UserRepositoryTest { Container static MySQLContainer? mysql new MySQLContainer(mysql:8.0); DynamicPropertySource static void configureProps(DynamicPropertyRegistry registry) { registry.add(spring.datasource.url, mysql::getJdbcUrl); registry.add(spring.datasource.username, mysql::getUsername); registry.add(spring.datasource.password, mysql::getPassword); } }给测试起一个“行为名”而不是“方法名”testGetUser()和shouldReturnUserWhenUsernameExists之间差的不仅仅是一行注释。测试方法的命名是对你业务规则的一种公开声明。强迫自己用“should...When...”的句式会让你在写测试前先理清这个方法的预期行为。你试过Naming Impossible吗当你写不出一个清晰的测试名往往意味着你的需求本身就是模糊的。名字写好了测试本身就是文档。新同事接手项目时读一遍测试的方法名几乎就能理解整个系统的业务骨架。这比读那堆过期的Swagger注解要有价值得多。覆盖率数字是陷阱不是勋章“我们项目测试覆盖率达到了90%”这句话往往是一个笑话。覆盖率的计算方式是基于行执行而不是基于行为验证。一个满是Mock的测试完全可以通过九成覆盖率的门槛但实际行为一个都没锁。把覆盖率当作KPI你会得到一个数字失去一个安全网。更值得关注的是“变异测试”——它通过故意在代码中引入缺陷比如把改成来看看测试能否识别出这种变化。如果测试仍然通过说明你的断言没有真正锁住该锁的逻辑。虽然SpringBoot生态中没有默认的变异测试工具但类似Pitest这样的插件可以轻松接入。能发现缺陷的测试才是好测试永远通过的测试只是一场合法的形式主义。测试不出BUG只是让BUG无处躲藏你见过那种“测试全绿一上线就黄”的项目吗多半是测试跑在了完全错误的抽象层级。比如在Service测试里连数据库在Controller测试里启动整个Spring容器在Repository测试里写了一大堆业务断言。单元测试的黄金法则是每一层只测自己的事其他的一律用假报文和Mock挡住。实践时还要注意单元测试和集成测试的物理隔离。在Maven或Gradle中把测试类按后缀区分——Test跑在fast profile下构建时默认执行IT跑在slow profile下专门做端到端验证不参与每次提交的快速反馈。这能避免整个构建被沉重的集成测试拖到十分钟而开发者为了偷懒连fast test都跳过。最快的构建是“跳过测试”吗不最快的构建是让开发者跑起来没负担的构建。写测试就是给未来的自己留一扇窗重构一个没有测试的类本质上是在悬崖边蹦迪。而有了单元测试你可以放心地调整内部实现只要测试还绿就不怕改坏功能。单元测试是这个行业里唯一允许你“破坏”之后还能重建的地方。每当你想说“这段代码太简单了不用测”问问自己假如这段代码明天因为框架升级炸了你怎么第一时间知道SpringBoot给了你这么多现成的注解、Mock工具、断言库和容器集成不写测试、敷衍地写测试才是真正的“结构性懒惰”。你的下一行代码值得一个测试在旁边守着。现在就去写第一个切片测试吧——从那个五分钟前你觉得“不需要保护”的方法开始。
返回列表