拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

JUnit 5实战指南:从单元测试到可视化报告的全链路构建

JUnit 5实战指南:从单元测试到可视化报告的全链路构建

1. 项目全景:从一根测试用例到一套可视化体系

1.1 为什么到了现在还要重读JUnit 5

我接手过一个很奇怪的项目:单元测试的数量有一千多个,每次CI跑完都是绿的,可上线前大家还是不敢直接把接口放出去,非要人工把核心链路点一遍。等我把测试代码拉下来看,问题立刻清楚了——绝大多数测试方法在@BeforeEach里偷偷连了真实数据库,断言写的是assertTrue(true),还有一些测试类注释写着"别动,动了就红"。这其实就是很多Java团队的真实状态:JUnit 5的名号听过,单元测试也写了,但测试资产并没有变成项目的安全网。

所以当我决定整理一份"JUnit 5实战图谱"的时候,想做的不是给大家再贴一遍官方文档,而是把Java单元测试从"会写注解"升级到"敢说核心逻辑有保障"。这篇内容会覆盖JUnit 5的体系结构、常用注解、参数化测试、Mock协作、覆盖率统计,以及最终把结果变成可视化报告的一整套链路。对刚入行的同学来说,它能帮你建立单元测试的基本盘;对写了两三年业务代码、却总觉得测试不可控的同学来说,里面的排错和可视化方案应该能解决不少实际问题。

这套内容的另一个落脚点是"可视化"。很多人一听可视化就想到大屏、图表、运维监控,但在测试领域,可视化同样重要。覆盖率报告、失败堆栈、用例执行树、耗时分布,这些信息如果只躺在终端日志里,那测试就只是给自己看的一堆绿色对勾;只有当它们变成团队能一起审视的报表和趋势,自动化测试才真正产生决策价值。这也是我把"可视化"放在标题里的原因。

1.2 这套东西到底解决什么问题

先明确一个判断:JUnit 5不是"换个写法再看一遍",它解决的问题比JUnit 4时代大得多。JUnit 4把测试框架做成了一个整体,你想跑什么都由它说了算;JUnit 5则拆成了平台、引擎、编程模型三层。平台负责在JVM上启动测试,Jupiter负责提供注解和断言API,Vintage则负责兼容老的JUnit 3/4用例。这么一拆,好处立刻出来了:一套CI流水线里,可以同时跑JUnit 5的新用例、JUnit 4的历史用例,甚至第三方框架只要实现了Platform接口也能参与进来。

这个架构演进对普通开发者的直接价值是什么?首先是测试代码不用背上历史包袱,旧测试可以平滑过渡;其次是扩展能力打开了。比如你想在每次测试前后自动开启事务回滚、想根据环境变量跳过某些用例、想把测试结果发送到消息队列,都可以通过JUnit 5的扩展模型来做,而不是像JUnit 4那样写一堆继承规则。

从团队管理角度讲,这套图谱还能回答一个问题:怎么衡量测试到底够不够。以前我们习惯用"测试类数量"来汇报进度,数字很虚。有了JaCoCo覆盖率可视化、Allure报告链路,我们能看的是行覆盖率、分支覆盖率、用例失败分布、耗时Top N这些真实指标。指标不一定完美,但至少比"我写了三百个测试"靠谱得多。

1.3 适合谁来用

第一类适合的是Java后端开发,尤其是做Spring Boot、微服务的人。业务代码写起来很爽,但回归全靠人肉,重构一次提心吊胆,这种状态太常见了。JUnit 5加Mockito可以帮你把Service层、工具类、接口参数处理这些逻辑牢牢锁住。第二类是测试开发或者想转型做质量保障的工程师,JUnit 5的扩展点、参数化、动态测试、报告集成,是搭一套公司级测试平台的底座。第三类是刚学Java的同学,框架那么多,从JUnit 5入手理解"断言驱动设计"是最不烧钱的方式,一个Java文件就能跑起来。

我更想说的是,这篇内容不是"照图施工"的说明书,而是我踩过坑之后留下的路线图。有些配置我第一次搭的时候花了整整一个下午,因为版本不兼容、中文乱码、Mock没生效的原因挤在一起;这些坑我不会让你再踩一遍。

2. 架构与工具选型:先想清楚再动手

2.1 三大模块到底怎么分工

JUnit 5整个生态最核心的就是三层结构。JUnit Platform是所有测试框架运行时的基础,它不认得你写的@Test注解,只负责加载引擎、调度测试、收集结果。JUnit Jupiter是新版本里的主角,注解、断言、扩展点都在这个模块里。JUnit Vintage是一个兼容引擎,专门用来跑JUnit 4和JUnit 3的老用例。

这个分层最大的意义在于"组合"。举例来说,一个老项目有三千条JUnit 4用例,你不可能一夜之间全改成新语法。这时候只需要在类路径里放上vintage-engine,老用例继续跑;新代码用JUnit 5写,两者在同一个报告里汇总。实测下来,这种过渡期方案非常稳,团队也不需要停摆去搞大重写。

还有一个容易忽略的点:JUnit Platform基于Java 8设计,函数式接口、lambda表达式这些都是它的底层能力。这意味着你在JUnit 5里会大量使用lambda写断言,比如assertThrows、动态测试。这个设计让测试代码比JUnit 4时代的匿名内部类短了一大截,可读性也上来了。

2.2 最少依赖配置长什么样

如果用的是Maven,最省事的做法是引入junit-jupiter聚合依赖。它已经帮你把junit-jupiter-api、junit-jupiter-params、junit-jupiter-engine一起带进来了,不需要再手动一个个加。需要注意版本号,这里强烈建议直接把junit-jupiter的版本升到5.10.x以上,老版本在参数化测试和@TempDir这些特性上体验差很多。

除了JUnit自己,还必须有Surefire插件做桥接。Maven里的测试执行不是JUnit自己完成的,而是Surefire负责扫描类目录、找到*Test.class、再交给JUnit Test Engine。很多新人遇到了"明明写了测试却显示无测试执行"的经典问题,八成是Surefire版本太低。建议用2.22.2以上,最好是3.1.x,对JUnit 5的支持才完整。

再配上两个关键工具:Mockito负责做测试替身,JaCoCo负责收集覆盖率。Mockito和JUnit 5之间需要mockito-junit-jupiter扩展包,这样你才能用@Mock、@InjectMocks这些注解。JaCoCo则是在Maven插件里配置好 agent 和 report 目标,测试一跑就会把覆盖率数据落到target/jacoco.exec,再生成报告页。

这一套凑齐以后,一个可复现、可测覆盖率、可读报告的最小骨架就出来了。我见过不少项目额外堆了一堆测试框架的依赖,其实在起步阶段,上面这几件足够了。

2.3 可视化工具该选哪几件

可视化在测试链路里要分三个层面看。第一层是IDE内的执行树,IDEA里能直接看到每个测试类的状态、耗时、失败信息,这是日常开发最常用的一层,不需要额外配置。第二层是JaCoCo覆盖率报告,它能生成HTML页面,把Java源码按真实执行的语句染成绿、黄、红,一眼看出哪些代码没有被执行。第三层是Allure报告,它把测试运行过程中的步骤、截图、日志、参数、注释搬到一个统一的Web页面上,特别适合做项目汇报和团队复盘。

选型逻辑并不复杂:如果你只是想自己确认代码质量,IDEA加上JaCoCo就够用。如果测试要给整个研发团队看,或者要在CI流水线里沉淀历史趋势,Allure值得投入。Allure的生态覆盖了JUnit 5、TestNG、pytest、Jest等一堆框架,将来就算团队引入别的语言,报告这层也可以复用。还有一点个人体会:覆盖率指标容易让大家陷入"数字游戏",Allure的步骤型报告更适合讲业务故事。测试不是只看绿红,要看业务路径有没有被真正走通。

2.4 为什么我建议团队直接切JUnit 5

技术上不存在"必须使用最新版本"这类规定,但JUnit 5相对JUnit 4的改进都是实打实的。JUnit 4时代的测试继承关系常常很复杂,公共方法放在父类里,子类一多,跑到最终可能你都不知道哪些场景被覆盖了。JUnit 5的@Nested直接在内部类里组织场景,父子关系清晰,报错的时候也能从执行树一看就懂。

参数化测试是另一个决定性的理由。JUnit 4里的Parameterized需要额外Runner、构造器传参,代码非常啰嗦;JUnit 5用@ParameterizedTest配@CsvSource、@MethodSource,十来行代码就能把"多组输入输出"测完,数据表格直接出现在报告里。再搭配assertAll聚合断言、@TempDir临时文件目录、@Timeout超时控制,日常业务测试的痛点基本都能覆盖到。

没有必须切的时候,但切了以后皮肤会明显变好。这行不是玩笑:一个项目从JUnit 4迁移到JUnit 5之后,测试类的平均代码量至少缩减三成,新人理解测试执行逻辑的速度也快了很多。

3. 基础实操:从空项目到第一簇绿色

3.1 初始化项目结构与依赖

我习惯先把项目结构画成一张图再动手,这样测试代码放哪儿、资源文件放哪儿都不会乱。一个典型的Maven工程是这样:

my-service/ ├── pom.xml └── src/ ├── main/ │ └── java/com/company/order/ │ ├── Order.java │ ├── OrderService.java │ └── OrderRepository.java └── test/ └── java/com/company/order/ └── OrderServiceTest.java

主代码和测试代码严格放到对应的src/main/java与src/test/java下,这是Maven的约定,也是Surefire扫描的基础。

pom.xml关键部分可以这样写:

<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <junit.version>5.10.2</junit.version> <mockito.version>5.11.0</mockito.version> </properties> <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>${mockito.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.2.5</version> </plugin> </plugins> </build>

看到没有,JUnit 5最小运行只需要一个Jupiter依赖加一个Surefire插件。这个配置一旦通了,后面所有高级功能都是在做加法。

3.2 生命周期注解的坑

生命周期是JUnit 5里最基础也最容易写错的部分。@BeforeAll在类中所有测试方法执行前运行一次,@AfterAll在所有测试完成后运行一次。默认情况下,@BeforeAll和@AfterAll必须是静态方法,否则启动直接报错。要是实在不想写成静态方法,可以在类上标注@TestInstance(Lifecycle.PER_CLASS),让JUnit用同一个实例执行所有测试,JUnit 4里那些"测试字段相互污染"的老问题必须重新审视。

每个测试方法的前置动作放在@BeforeEach和@AfterEach里,它们在每条测试方法前后都会执行。这里我吃过一个亏:在@BeforeEach里初始化了重量级客户端,比如Redis连接、外部HTTP客户端,结果每一个测试方法都重新连一次,整套测试跑下来慢得吓人。正确做法是重量级资源放@BeforeAll,轻量级数据准备放@BeforeEach。

一套标准写法是这样:

class OrderServiceTest { private OrderService orderService; private FakeOrderRepository repository; @BeforeEach void setUp() { repository = new FakeOrderRepository(); orderService = new OrderService(repository); } @AfterEach void tearDown() { repository.clear(); } @Test @DisplayName("订单总额应该正确累加多个商品金额") void shouldCalculateTotalAmount() { repository.save(new Order("A001", List.of( new Item("牛奶", new BigDecimal("15.50")), new Item("面包", new BigDecimal("6.00")) ))); BigDecimal total = orderService.computeTotal("A001"); assertEquals(new BigDecimal("21.50"), total); } }

这里面最需要注意的是:测试方法名不要用test01、test02这种编号。我宁愿长一点,比如shouldThrowExceptionWhenOrderNotFound,这样看报告时不需要翻代码,测试名本身就是文档。@DisplayName则可以用来补充中文场景说明,适合展示给非技术同事看。

3.3 命名与组织测试:别小看可读性

测试代码的命名风格影响的是排障效率,不是运行速度。我见过太多OrderServiceTest里几十个test1、test2,失败时报错行号一出来,根本不知道是哪条业务规则被破坏了。给团队定一个简单的约定:类名用被测类名加Test,方法名用should...When...句式,展示名用@DisplayName写业务场景。

组织上还有一个容易被忽略的做法:一个测试类尽量只测一个类,一个测试方法尽量只验证一个行为。混合多个业务逻辑的测试方法,一旦失败,你还需要看代码才知道是哪个逻辑挂了;拆开以后,测试报告会直接告诉你哪条规则出了问题。这套纪律坚持下来,测试资产的长期维护成本会低很多。

4. 高级特性:把重复用例压缩到极致

4.1 参数化测试:别再复制粘贴测试方法

单元测试写多了,最大的敌人就是复制粘贴。一个接口要测空值、边界值、正常值、超大值,有些人直接就写了四个几乎一模一样的方法。JUnit 5的@ParameterizedTest就是为这类场景准备的。

拿最简单的加法逻辑举例子:

class SimpleCalculatorTest { private final SimpleCalculator calculator = new SimpleCalculator(); @ParameterizedTest @CsvSource({ "1, 2, 3", "10, 20, 30", "0, 0, 0", "-1, 2, 1" }) void shouldAddTwoNumbers(int a, int b, int expected) { assertEquals(expected, calculator.add(a, b)); } @ParameterizedTest @MethodSource("provideAmounts") void shouldConvertAmountToCents(int yuan, long expectedCents) { assertEquals(expectedCents, MoneyConvertor.toCents(yuan)); } static Stream<Arguments> provideAmounts() { return Stream.of( Arguments.of(0, 0L), Arguments.of(1, 100L), Arguments.of(100, 10000L) ); } }

这里的@CsvSource适合数据量不大、格式简单的场景;@MethodSource适合更复杂的结构化数据。我一般是优先用@MethodSource,因为方法里可以写中文注释,还可以动态构造边界值,团队能够理解"这些用例到底在防什么"。参数化测试跑完以后,Allure报告里每组参数会变成单独一行,哪组数据挂了清清楚楚,这是它比循环断言高明的地方。

4.2 嵌套测试和动态测试:让执行树长出结构

@Nested是一个被很多人低估的特性。它允许你在测试类内部声明非静态内部类,再在这些内部类里继续写测试方法或下一层嵌套。这样做最直观的收益是:IDEA的测试执行树会呈现出一种"业务模块-子场景-具体用例"的层级关系,报告阅读体验大幅提升。

class PromotionServiceTest { @Nested @DisplayName("满减活动") class FullReduction { @Test @DisplayName("门店订单可以叠加门店满减") void shouldApplyStorePromotion() { // 测试省略 } } @Nested @DisplayName("折扣券") class Coupon { @Test @DisplayName("折扣券和满减不能同时使用") void shouldRejectDuplicatePromotion() { // 测试省略 } } }

嵌套类里可以直接访问外部类的字段和@BeforeEach逻辑,这非常方便,但必须注意:嵌套类本身默认不执行外部类里那些不继承的测试逻辑,你需要按场景规划好初始化代码。@TestFactory则适合生成一批在运行时才确定的测试用例,比如读文件来生成用例、根据配置项生成用例。它返回DynamicTest列表,执行树里同样能展示每个动态用例的名字和结果。

这两块能力我建议在真实业务里按需使用,不要迷信。@Nested适合业务层次分明的场景,动态测试适合数据驱动但有额外创建逻辑的场景。它们带来的最大改变是让"测试意图"变得可视化,而不是让测试数量变得更多。

4.3 断言与异常:从 assertEquals 到 assertAll

assertEquals是基本功,但JUnit 5真正提升的是"聚合断言"和"异常断言"。我在测试一个对象时,以前顺序写七八个assert*,如果第一个就失败了,后面全都不执行,你只能看到最表层的问题。改成assertAll以后,所有断言都会执行,失败点会一次性汇总在报告里:

@Test @DisplayName("订单查询结果应该完整") void shouldReturnFullOrderInfo() { Order result = orderService.query("A001"); assertAll("order data check", () -> assertEquals("A001", result.getId()), () -> assertEquals("待付款", result.getStatus()), () -> assertEquals(2, result.getItemCount()), () -> assertNotNull(result.getCreatedAt()) ); }

异常测试以前用JUnit 4的@Test(expected = ...),语义不够直观。JUnit 5推荐用assertThrows,它还能拿到异常对象做进一步校验:

@Test @DisplayName("订单不存在时应该抛出明确异常") void shouldThrowWhenOrderMissing() { IllegalArgumentException ex = assertThrows( IllegalArgumentException.class, () -> orderService.computeTotal("NOT_EXIST") ); assertTrue(ex.getMessage().contains("订单不存在")); }

这里不需要为每一种异常类型做花哨的测试,但有两点值得注意:一是尽量断言异常消息里的关键信息,而不仅仅是异常类型,否则两个不同的报错会被当成同一个问题处理;二是用assertTimeout做超时断言时不要让它进入生产代码的单测逻辑里去依赖真实时钟,JUnit的@Timeout可以独立于业务代码,单独守护测试本身的运行时间。

4.4 Mock和隔离:测试不依赖数据库也能落地

单元测试的"单元"边界必须在代码层面划清。如果每次跑测试都需要一个MySQL实例,那这套测试根本没法在CI里稳定运行,更别提并行执行。Mockito是Java里主流的选择,它负责替身掉那些"外部且昂贵"的依赖。

使用Mockito和JUnit 5的推荐姿势是@ExtendWith(MockitoExtension.class),这个扩展可以让@Mock、@InjectMocks自动生效:

@ExtendWith(MockitoExtension.class) class OrderServiceWithMockTest { @Mock OrderRepository repository; @InjectMocks OrderService orderService; @Test @DisplayName("查询订单时应该返回包装后的聚合对象") void shouldReturnWrappedOrder() { when(repository.findById("A001")).thenReturn(Optional.of(createOrder())); OrderView view = orderService.getView("A001"); assertEquals("A001", view.getOrderId()); verify(repository).findById("A001"); } }

@Mock创建了假仓库,@InjectMocks把它注入到被测类里。这里有个常见的误区:when(...)和verify(...)不要同时出现大量重复。测试里最该关注的是"被测逻辑自己做了什么",对外部依赖的交互做一次关键校验就够了,不需要把每个调用细节都锁死,否则重构时Mock断言会成为新的负担。真正注入不进来的依赖,比如静态方法、私有方法,那已经不是单元测试的职责,应该借助重构手段让代码更可测,而不是去搞字节码魔法。

5. 可视化落地:覆盖率、报告和CI

5.1 用JaCoCo把"没测到的地方"点亮

JaCoCo是我在项目里常配的覆盖率工具,它的原理是在类加载时对字节码做插桩,记录哪些语句被执行过。测试跑完后,把jacoco.exec文件解析成HTML报告,你就能清楚看到每个类的行覆盖率和分支覆盖率。

Maven里的配置只需要三段:

<plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.12</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin>

执行完mvn clean test之后,进target/site/jacoco/index.html,首页就是各个包的覆盖情况。绿色代表执行过的语句,黄色表示部分覆盖,红色是完全没跑到的。这个页面有几种用法:一是看核心业务包的行覆盖率,别只看总包粒度;二是看分支覆盖,分支覆盖率低说明if else和异常路径根本没有用例兜底;三是结合代码变更看差异,新代码补充了哪些覆盖,这是团队Code Review时的最佳素材。

JaCoCo确实有偏差,它只看代码有没有执行,不看断言质量好坏。所以我在团队里把JaCoCo定位成"底线指标",覆盖率不是越高越好,但核心模块低于50%肯定是有问题的。需要提醒的是,不要为了凑覆盖率去写一堆只执行不验证的空走查测试,那比覆盖率低更可怕。

5.2 用Allure把测试结果变成项目报告

JaCoCo回答的是"哪些代码没测到",Allure回答的是"这些测试跑得怎么样、为什么失败"。Allure的报告页面会展示每个测试步骤、参数、日志、时长、失败原因,信息密度和颜值都在线,团队做迭代复盘时一眼就能看懂。

接入Allure也不复杂,引入allure-junit5依赖,然后在src/test/resources/junit-platform.properties里开启自动适配,测试跑完后结果会落在target/allure-results目录。再用命令行把结果变成HTML报告:

mvn clean test allure generate target/allure-results -o target/allure-report --clean allure open target/allure-report

没有Allure命令行工具也可以做静态部署,直接开启项目里的静态服务器指向报告目录即可。我实际用下来觉得,Allure真正厉害的地方是它把测试的"执行故事"拼了起来,每个用例有前置步骤、请求参数、预期值、实际值,测试失败时不再需要人去翻代码猜原因。这种可视化能力在给管理层看"测试能不能支撑上线"的时候尤其重要。

5.3 做好CI的最后一公里

本地能跑通一套测试,和CI上每次提交自动跑、自动出报告,完全是两回事。CI里首先要保证执行命令是幂等的:清掉旧的target,跑mvn clean verify,不要在流水线里依赖本地环境变量。

我常用的极简CI脚本大概是这样:

mvn clean verify \ -Dtest=*Test \ -DfailIfNoTests=true \ -Dmaven.test.failure.ignore=false

-DfailIfNoTests=true很关键,它能防止"一个测试都没扫描到但构建还是通过"的假平安。如果团队规定了覆盖率红线,可以在JaCoCo里配check目标,低于阈值直接fail构建。这一步会有点痛苦,因为刚开始很多历史模块不可能一天达标,建议先对新增代码设置阈值,不用一把抓全项目。

测试报告静物放在CI产物里基本没人看,一定要挂到团队常用平台或者消息通知里。比如流水线结束后把Allure报告地址贴进群里,失败用例自动@相关模块的负责人。可视化落到这一步,测试才从个人手段变成了团队制度。

6. 常见问题与排错实录

6.1 测试全绿但心里没底:查这四处

被"假绿色"坑过的人都知道一个朴素的教训:测试绿色只能证明"没有断言失败",不能证明"该断言的都断言了"。遇到全绿但心里没底的项目,我一般先做四步排查。第一,打开测试代码,搜索assertTrue(true)、assertNotNull("字符串常量")这类空转断言,看有多少条;第二,用JaCoCo看核心包的行覆盖率,如果用例很多但覆盖率百分之二三十,八成是测试压根没有触达真实逻辑;第三,看@BeforeEach里是否连了数据库和外部服务,连了外部资源的测试在CI换一台机器跑可能就挂了,绿色不可复现;第四,检查@Disabled的数量,如果一堆用例被注释成"暂时跳过",那这堆绿色就是团队选择性忽视的结果。

排查完之后,我的整治顺序永远是:先补断言、再补边界、最后补覆盖率指标。最怕的是先拿JaCoCo的红线去压团队,大家会在报告上做表面功夫,测试质量反而下降。

6.2 构建和运行时的几个经典坑

JUnit 5虽然上手快,但配置层面的坑坑洼洼还是不少。我把自己踩过的和帮别人排查过的问题整理成了一份速查表:

现象常见原因解决方向
运行mvn test显示无测试执行Surefire版本过低,不认识JUnit 5升级Surefire到3.x
@Test注解找不到依赖里只引了jupiter-api,缺engine引入junit-jupiter聚合依赖
@BeforeAll报"must be static"默认生命周期是PER_METHOD把方法改成static,或开PER_CLASS
Mockito的@Mock不生效缺少mockito-junit-jupiter扩展引入对应包,并加@ExtendWith
中文测试名在报表里乱码编译和测试运行编码不一致在POM或CI里统一UTF-8参数
CI跑挂但本地全绿依赖外部Redis/MySQL用Mock代替外部依赖

中文乱码这个坑很典型。本地IDE默认UTF-8,CI服务器可能还是平台默认编码,编译时中文字符串就变了。我的习惯是在POM里显式声明编码参数:

<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties>

宁可多写两行配置,也别让乱码吃掉你排查问题的精力。

6.3 回城实测:一个真实微服务订单模块

最后放一个真实案例。之前要验证一个订单服务里的金额计算方法,它依赖库存服务、优惠券中心、用户等级三个外部接口。如果靠单元测试全部接真实服务,环境不完整,测试基本跑不起来。最后我们定了这样的方案:订单金额计算用JUnit 5的参数化测试覆盖正常价、折扣价、满减叠加、捆绑促销、特殊会员价;三个外部依赖全部用Mockito的when桩稳返回;每条用例独立构造数据,互不共享;业务异常用assertThrows验证;关键分支用assertAll一起看;跑完以后配JaCoCo查看分支覆盖,再用Allure把每一步请求参数放进报告。

迁移完以后效果很明显。以前手工联调一单要五分钟,现在mvn test十六秒跑完,而且失败时直接告诉你哪组参数、哪条规则出了问题。这个案例里没有高深技术,就是把JUnit 5的基础和高级特性按场景组合了起来。单元测试真正做扎实以后,你会发现重构顺手了、交付焦虑也下来了。团队逐渐形成习惯后,我对新需求的第一个要求就是:先把核心逻辑的单测补上再提测,这个门槛守住了,后面很多问题都在源头被拦截了。

返回列表