用好 JUnit 5 的高级特性:提升单测效率和质量
<div data-page-id="RJGpdSPpPo95uuxorMQc1AN4nnb" data-lark-html-role="root" data-docx-has-block-data="true"><h1 class="heading-1 ace-line old-record-id-NVQNd4p6Boh4Rzxyl0Tc15dMnck">写在前面</h1>
<div class="ace-line ace-line old-record-id-R1Krd6WyXo7iTXxgV3ZcSXl7nMd">在当今的软件开发实践中,单元测试已成为保障代码质量的必备环节。许多团队已经积累了一定的单元测试经验,能够编写基本的测试用例来验证功能逻辑。然而,当我们面对复杂的业务场景时,仅靠基础的JUnit功能往往会导致测试代码冗长、结构混乱,甚至出现大量重复代码。</div>
<div class="ace-line ace-line old-record-id-DnHsdB8xnoSl0jxxeRccqf4pnSe">作为最新版本的Java测试框架,JUnit 5引入了许多强大的高级特性,可以帮助我们编写更优雅、更高效的单元测试。本文将探讨JUnit 5的这些高级特性,并以案例的形式展示如何利用它们,以提升单元测试的质量和开发效率。</div>
<div class="ace-line ace-line old-record-id-K9hOd4u2aoz9XGxrSVnczu6tn8f"> </div>
<h1 class="heading-1 ace-line old-record-id-Ky7LdYFiNoxE9MxM48uc8NjvnIh">使用注解@DisplayName给测试方法命名</h1>
<h2 class="heading-2 ace-line old-record-id-IlaEd3ljDocpakxKs14cIYZKnzh">适用场景</h2>
<div class="ace-line ace-line old-record-id-YF9CdLCHsoS8RExRjVccxIbcnHg">在传统的单元测试中,测试方法的命名往往受到Java方法命名规则的限制,不得不使用驼峰命名法或下划线连接单词,如testGetUserByIdWithInvalidId()。这样的名称虽然能表达测试意图,但在测试报告中可读性并不理想。</div>
<div class="ace-line ace-line old-record-id-IRsZd1qEgo2ppDxG8D7cBvdbneh">@DisplayName注解允许我们为测试类和测试方法提供更具可读性的名称,支持空格、特殊符号甚至emoji表情。这样生成的测试报告更加直观,便于团队快速理解每个测试的意图。</div>
<h2 class="heading-2 ace-line old-record-id-BXzmddPGtoz3JsxjiRuchBsanUf">使用示例</h2>
<pre class="ace-line ace-line old-record-id-SIsmdyuF4oXXeexSs3SccYa7nbc"><code class="language-Java" data-lark-language="Java" data-wrap="false">@DisplayName("用户服务测试")
class UserServiceTest {
@Test
@DisplayName("根据ID获取用户 - 当ID无效时抛出异常")
void testGetUserByIdWithInvalidId() {
// 测试逻辑
}
@Test
@DisplayName("创建用户 - 成功场景 ✅")
void testCreateUserSuccessfully() {
// 测试逻辑
}
}</code></pre>
<div class="ace-line ace-line old-record-id-UdZJdk0kco490nxSrLGceOswngG">在IDE或构建工具生成的测试报告中,你会看到更具描述性的测试名称,而不是原始的方法名。这对于大型项目中的测试维护特别有帮助,新成员可以快速理解每个测试的意图。</div>
<div class="ace-line ace-line old-record-id-N1jWdcZ0eofRT6xoMLecXr7QnMe"> </div>
<h1 class="heading-1 ace-line old-record-id-FtXpd4DzKoGv0vx62yNc0N4XnLf">使用嵌套注解@Nested组织代码</h1>
<h2 class="heading-2 ace-line old-record-id-CVBFdSZvuoexubxCEdncoZ77njc">适用场景</h2>
<div class="ace-line ace-line old-record-id-PHvxdHpTLo4CBDxrRbAcZd56nAa">随着业务逻辑的复杂性增加,单个测试类中可能包含大量测试方法,这些方法往往针对同一功能的不同场景或边界条件。传统的平铺结构会使测试类变得臃肿,难以维护。</div>
<div class="ace-line ace-line old-record-id-LSMWdT6sMo5EP7xmekhcVtYcnUc">@Nested注解允许我们在测试类中创建嵌套的测试类,从而以层次化的方式组织测试代码。这种方式特别适合描述"给定-当-那么"(Given-When-Then)的测试模式,使测试结构更加清晰。</div>
<div class="zoneType-calloutBlock old-record-id-HQkRdUwqLoj8qGxRvINcpFk4nce">
<div class="callout-container" data-emoji-id="pencil2">
<div class="callout-block">
<div class="ace-line ace-line old-record-id-UzerdnVZXoiGldxzXWOcdTNfn47">"给定-当-那么"(Given-When-Then),这种测试模式是非常经典的,我们平时也在不经意间采用了。使用示例</div>
</div>
</div>
</div>
<div class="ace-line ace-line old-record-id-Wg4Ddz692oMm85xUDU7cdCuTnzh">考虑一个订单处理服务的测试场景:</div>
<pre class="ace-line ace-line old-record-id-ElybdVbDKopUT5xxielcr3FXnah"><code class="language-Java" data-lark-language="Java" data-wrap="false">@DisplayName("订单服务测试")
class OrderServiceTest {
@Nested
@DisplayName("创建订单")
class CreateOrder {
@Test
@DisplayName("当库存充足时 - 成功创建订单")
void whenStockSufficient_thenCreateOrderSuccessfully() {
// 测试逻辑
}
@Test
@DisplayName("当库存不足时 - 抛出异常")
void whenStockInsufficient_thenThrowException() {
// 测试逻辑
}
}
@Nested
@DisplayName("取消订单")
class CancelOrder {
@Test
@DisplayName("当订单状态为新订单时 - 成功取消")
void whenOrderStatusIsNew_thenCancelSuccessfully() {
// 测试逻辑
}
@Test
@DisplayName("当订单状态为已发货时 - 不允许取消")
void whenOrderStatusIsShipped_thenNotAllowCancel() {
// 测试逻辑
}
}
}</code></pre>
<div class="ace-line ace-line old-record-id-UbWJdcI7Io8IRGxn1ZRcsaTOnCd">这种嵌套结构清晰地表达了测试的组织逻辑,每个嵌套类代表一个功能点,每个测试方法代表该功能下的一个具体场景。</div>
<h2 class="heading-2 ace-line old-record-id-Iqe5d0RgsoT9OexWwI1cvmUDndd">注意事项</h2>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-C0cTdu61joOFutxCbivcsTcGn5c" data-list="bullet">嵌套层级控制:建议嵌套层级不超过3层,过深的嵌套会降低可读性。</li>
<li class="ace-line ace-line old-record-id-Gc8rdRWmMofKwyx8Jm5caq48nQb" data-list="bullet">生命周期方法:嵌套类可以有自己的@BeforeEach和@AfterEach方法,但不会继承外部类的这些方法。</li>
<li class="ace-line ace-line old-record-id-T2UBdDn7PoPB7zxlfjYcwNCDnof" data-list="bullet">共享资源:如果需要在外部和嵌套类之间共享资源,可以考虑使用@BeforeAll在外部类中初始化。</li>
</ul>
<div class="ace-line ace-line old-record-id-NtDXdNG0AoKP35xWAwvcUUVLnCf"> </div>
<h1 class="heading-1 ace-line old-record-id-P4SJdkDEYoQLeixEixWc7rd6nAg">使用注解@RepeatedTest进行重复测试</h1>
<h2 class="heading-2 ace-line old-record-id-SFYZdomCDo9SI4xCQYlcBMwonod">适用场景</h2>
<div class="ace-line ace-line old-record-id-WgqrdDyefo8XUYxCfFMcaDuYnte">在测试中,有些场景需要验证代码的幂等性或稳定性,例如:</div>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-LMgGdIcWRoEqSwxedhScPtxwnUM" data-list="bullet">验证随机数生成的质量</li>
<li class="ace-line ace-line old-record-id-Sg7MdSJFtoxBxNxMNLPcsT4JnIg" data-list="bullet">测试并发安全性</li>
<li class="ace-line ace-line old-record-id-Wm51duyLSoYJo4xNDlNcJo6enlh" data-list="bullet">验证资源清理是否彻底</li>
<li class="ace-line ace-line old-record-id-XqIWdN33yoDtY9xVRvEcCrd1nvb" data-list="bullet">检测偶发的竞态条件</li>
</ul>
<div class="ace-line ace-line old-record-id-PFwidiV4ooiA4mxWMBecJsVBnOc">@RepeatedTest注解允许我们轻松地重复运行同一个测试多次,而不需要编写循环或重复代码。</div>
<h2 class="heading-2 ace-line old-record-id-Eqccd5wqOowatVx1QeRcuZnRn7f">使用示例</h2>
<pre class="ace-line ace-line old-record-id-DRr6dTH8So3JWmx81o1cY4l8nIf"><code class="language-Java" data-lark-language="Java" data-wrap="false">@DisplayName("随机数生成测试")
class RandomNumberTest {
@RepeatedTest(value = 100, name = "第{currentRepetition}次测试,共{totalRepetitions}次")
@DisplayName("验证随机数在合理范围内")
void testRandomNumberInRange(RepetitionInfo repetitionInfo) {
int random = RandomUtils.nextInt(1, 101);
assertTrue(random >= 1 && random <= 100,
() -> "第"repetitionInfo.getCurrentRepetition() + "次测试失败: "random);
}
}</code></pre>
<div class="ace-line ace-line old-record-id-EXw8dm83FoMkL1xM6bccgj7JnEe">这个测试会运行100次,每次都会生成一个1-100的随机数并验证其范围。如果任何一次测试失败,报告中会明确指出是哪一次运行失败。@RepeatedTest支持以下配置:</div>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-BPGtdXRAioj6WOxgx2RcdkjFnYe" data-list="bullet">value:指定重复次数</li>
<li class="ace-line ace-line old-record-id-S3aQdVudoo07yrx2izXcKrxan0P" data-list="bullet">name:自定义测试显示名称,可以使用{currentRepetition}和{totalRepetitions}占位符</li>
<li class="ace-line ace-line old-record-id-OhEAdhPWioEeGbxNF9ZcNgOin9d" data-list="bullet">可以通过RepetitionInfo参数获取当前重复信息</li>
</ul>
<div class="ace-line ace-line old-record-id-ZCFRd7vito93NIxAjvlcGx5Lnse"> </div>
<h1 class="heading-1 ace-line old-record-id-WDcLdcEdNoPin1xgQtncVWXlneg">使用参数化测试,避免大量重复代码</h1>
<h2 class="heading-2 ace-line old-record-id-C535d47e7oGZuExK8J3cSpBhnae">适用场景</h2>
<div class="ace-line ace-line old-record-id-AzggdWVfJoVXM4xuvLbcKcu6nge">在测试中,我们经常需要对同一逻辑使用多组输入数据进行验证。传统做法是编写多个几乎相同的测试方法,或在一个测试方法中使用循环。这两种方式都有缺点:前者产生大量重复代码,后者在第一次失败后就停止测试。</div>
<div class="ace-line ace-line old-record-id-KfNbd387TofbMBxfHTdcGlWen9c">JUnit 5的参数化测试功能可以优雅地解决这个问题,它允许我们定义一个测试方法,然后为其提供多组参数,每组参数都会作为独立的测试用例运行。这里的多种参数,以数据源的形式提供。</div>
<h2 class="heading-2 ace-line old-record-id-CzoJdkmp9o0WYax2uMGcQqmAnyc">各类数据源</h2>
<div class="ace-line ace-line old-record-id-Rf8mdhuZWoeFA5x9eexckeIZnwd">JUnit 5提供了多种参数来源,分别介绍一下。</div>
<h3 class="heading-3 ace-line old-record-id-UWebdtRNuoeiW6xaYLLcIOognwc">@ValueSource 基础数据源</h3>
<div class="ace-line ace-line old-record-id-JRhCdOQLhoZBoGxypq0cYyFdnvf">@ValueSource 是最简单的参数提供方式,适用于基本数据类型的测试:</div>
<pre class="ace-line ace-line old-record-id-IdGrdZhtPoWlT3x5GHocVO6Kn8d"><code class="language-Java" data-lark-language="Java" data-wrap="false">@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15})
@DisplayName("测试奇数验证")
void testIsOdd(int number) {
assertTrue(MathUtils.isOdd(number),
() -> number + " 应被识别为奇数");
}
@ParameterizedTest
@ValueSource(strings = {"racecar", "radar", "madam"})
@DisplayName("回文字符串验证")
void testPalindrome(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@ParameterizedTest
@ValueSource(doubles = {1.5, 2.0, 3.8})
@DisplayName("双精度数验证")
void testDouble(double num) {
assertTrue(num > 1.0);
}</code></pre>
<div class="ace-line ace-line old-record-id-JnOodAzEqoWt18xVUrgceXWgnEh"> </div>
<h3 class="heading-3 ace-line old-record-id-RSiBd0Fj9oPxSNxHfQhcZ0V6nEe">@EnumSource 数据源</h3>
<div class="ace-line ace-line old-record-id-OCXKdhBY6oyzgix58fKciEpJnhd">当需要测试枚举所有值时,@EnumSource非常高效:</div>
<pre class="ace-line ace-line old-record-id-LvRgdDIhyoaACgxUGLic52iInhb"><code class="language-Java" data-lark-language="Java" data-wrap="false">enum Status {
NEW, PROCESSING, COMPLETED, CANCELLED
}
@ParameterizedTest
@EnumSource(Status.class)
@DisplayName("测试所有状态转换")
void testStatusTransition(Status status) {
assertDoesNotThrow(() -> OrderService.transitionStatus(status));
}
// 测试枚举子集
@ParameterizedTest
@EnumSource(value = Status.class, names = {"NEW", "PROCESSING"})
@DisplayName("测试可编辑状态")
void testEditableStatus(Status status) {
assertTrue(OrderService.isEditable(status));
}
// 模式匹配排除枚举值
@ParameterizedTest
@EnumSource(value = Status.class, mode = EXCLUDE, names = {"CANCELLED"})
@DisplayName("测试非取消状态")
void testNonCancelledStatus(Status status) {
assertNotEquals("CANCELLED", status.name());
}</code></pre>
<div class="ace-line ace-line old-record-id-RyhjdVs1AoIOhCxOMcjcDeiEnZd"> </div>
<h3 class="heading-3 ace-line old-record-id-YSrCdT15ooTFMoxLgRyc7HQ2nXe">@NullSource 和 @EmptySource 数据源</h3>
<div class="ace-line ace-line old-record-id-CUfndQPODoCWMoxPilpcRwKCnQZ">边界值测试是确保代码健壮性的重要手段,@NullSource和@EmptySource专门用于测试空值和空集合场景:</div>
<pre class="ace-line ace-line old-record-id-I722d3yJdo3odKx9zwzcTmXBnmb"><code class="language-Java" data-lark-language="Java" data-wrap="false">@ParameterizedTest
@NullSource
@DisplayName("测试处理null输入")
void testWithNullInput(String input) {
assertThrows(IllegalArgumentException.class,
() -> StringUtils.calculateLength(input));
}
@ParameterizedTest
@EmptySource
@DisplayName("测试处理空字符串")
void testWithEmptyString(String input) {
assertEquals(0, StringUtils.calculateLength(input));
}
// 组合使用
@ParameterizedTest
@NullAndEmptySource
@DisplayName("测试处理null和空字符串")
void testWithNullAndEmpty(String input) {
assertTrue(input == null || input.isEmpty());
}</code></pre>
<div class="ace-line ace-line old-record-id-OqBddlafdo4GWJx1uhScriE6nxe"> </div>
<h3 class="heading-3 ace-line old-record-id-QSe1dCUMxo15itxblAAcbH2YnMk">@CsvSource 结构化数据源</h3>
<div class="ace-line ace-line old-record-id-MlxXdKAmWoQoS3xx5YOcyfXFnoe">@CsvSource 适合需要多参数的测试场景:</div>
<pre class="ace-line ace-line old-record-id-WYC3dlFYYoYuqkxFQE3cMW1qnke"><code class="language-Java" data-lark-language="Java" data-wrap="false">@ParameterizedTest
@CsvSource({
"1, 1, 2", // 正常加法
"2, 3, 5", // 正常加法
"10, -5, 5",// 正负相加
"0, 0, 0" // 零值相加
})
@DisplayName("加法运算测试")
void testAdd(int a, int b, int expected) {
assertEquals(expected, MathUtils.add(a, b),
() -> String.format("%d + %d 应等于 %d", a, b, expected));
}
// 支持不同类型参数
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'', 0"
})
@DisplayName("字符串长度测试")
void testStringLength(String input, int expected) {
assertEquals(expected, input.length());
}
// 使用特殊分隔符
@ParameterizedTest
@CsvSource(delimiter = '|', value = {
"John Doe | 30 | true",
"Alice | 25 | false"
})
@DisplayName("用户验证测试")
void testUserValidation(String name, int age, boolean isAdult) {
assertEquals(isAdult, age >= 18);
}</code></pre>
<div class="ace-line ace-line old-record-id-QdwhdyDi4o99d2xAObocduVtnKb"> </div>
<h3 class="heading-3 ace-line old-record-id-R5LLdkWVZorOJmxiVXRcTcVLnbb">@CsvFileSource 数据源</h3>
<div class="ace-line ace-line old-record-id-Zr8Tdpf7JoVI00xe9wWcevKhnqd">对于大量测试数据,使用外部CSV文件(假设路径为/test-data/add_test_cases.csv)更便于维护:</div>
<pre class="ace-line ace-line old-record-id-ZjyKdg6qUo6rCCxp1LXcb6e8nNb"><code class="language-Java" data-lark-language="Java" data-wrap="false">addend1,addend2,sum
1,1,2
2,3,5
-5,5,0
1000000,1000000,2000000</code></pre>
<pre class="ace-line ace-line old-record-id-WDvsdINJtouV9wxTXAncbGkNn9g"><code class="language-Java" data-lark-language="Java" data-wrap="false">@ParameterizedTest
@CsvFileSource(resources = "/test-data/add_test_cases.csv", numLinesToSkip = 1)
@DisplayName("CSV文件数据驱动加法测试")
void testAddWithCsvFile(int addend1, int addend2, int sum) {
assertEquals(sum, Calculator.add(addend1, addend2),
() -> String.format("%d + %d 应等于 %d", addend1, addend2, sum));
}
// 使用不同分隔符的CSV文件
@ParameterizedTest
@CsvFileSource(resources = "/test-data/user_test_cases.tsv", delimiter = '\t')
void testUserImport(String username, String email, boolean expectedValid) {
assertEquals(expectedValid, UserValidator.isValid(username, email));
}</code></pre>
<div class="ace-line ace-line old-record-id-DnvMd5BYjovqb3xGBgVcrWpZn4g"> </div>
<h3 class="heading-3 ace-line old-record-id-Bf2ddUEFxoyg1mxSU4jcM2wPnpg">@MethodSource 复杂数据源</h3>
<div class="ace-line ace-line old-record-id-ZO4bdGdbEoEOwgx0g6Scj6V2nXf">@MethodSource 适用于需要动态生成复杂参数的场景:</div>
<pre class="ace-line ace-line old-record-id-GsV5d1mJkotRmNxfgKccqzscnzd"><code class="language-Java" data-lark-language="Java" data-wrap="false">@ParameterizedTest
@MethodSource("stringProvider")
@DisplayName("字符串长度验证")
void testLength(String input, int expectedLength) {
assertEquals(expectedLength, input.length(),
() -> "'"input + "' 的长度应为 "expectedLength);
}
// 基础数据提供方法
static Stream<Arguments> stringProvider() {
return Stream.of(
Arguments.of("hello", 5),
Arguments.of("world", 5),
Arguments.of("", 0),
Arguments.of("", 2)
);
}
// 复杂对象测试
@ParameterizedTest
@MethodSource("userProvider")
@DisplayName("用户年龄验证")
void testUserAge(User user, boolean expected) {
assertEquals(expected, user.isAdult());
}
static Stream<Arguments> userProvider() {
return Stream.of(
Arguments.of(new User("Alice", 25), true),
Arguments.of(new User("Bob", 17), false),
Arguments.of(new User("Charlie", 18), true)
);
}
// 多参数组合测试
@ParameterizedTest
@MethodSource("rangeProvider")
@DisplayName("数字范围验证")
void testInRange(int number, int min, int max, boolean expected) {
assertEquals(expected, MathUtils.isInRange(number, min, max));
}
static Stream<Arguments> rangeProvider() {
return Stream.of(
Arguments.of(5, 1, 10, true),
Arguments.of(15, 1, 10, false),
Arguments.of(0, 0, 0, true)
);
}</code></pre>
<div class="ace-line ace-line old-record-id-Ix5zde2bCoGNIHxCRQic90vGncg"> </div>
<h2 class="heading-2 ace-line old-record-id-K4hodUElHoqHLQxKg66cJkHvnsE">组合使用多种数据源</h2>
<div class="ace-line ace-line old-record-id-C3chdsrq8oy0Vox2ts8c8gGxnsb">可以组合多个数据源进行更全面的测试:</div>
<pre class="ace-line ace-line old-record-id-Y1J2dm6ktouEqCx21yrccb1enSg"><code class="language-Java" data-lark-language="Java" data-wrap="false">@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"", "\t", "\n"})
@DisplayName("测试各种空白输入")
void testBlankInputs(String input) {
assertTrue(StringUtils.isBlank(input));
}
@ParameterizedTest
@EnumSource(TimeUnit.class)
@ValueSource(ints = {1, 5, 10})
@DisplayName("测试时间单位转换")
void testTimeUnitConversion(TimeUnit unit, int value) {
assertNotNull(unit.toMillis(value));
}</code></pre>
<div class="ace-line ace-line old-record-id-E74Fd0lkpovmElxAh5Ocarkonvb"> </div>
<h2 class="heading-2 ace-line old-record-id-WPEBdo4FgoH9tgxHJPQc3tXHnKd">各种数据源对比</h2>
<div>
<table class="ace-table" data-ace-table-col-widths="153;153;153;153"><colgroup><col width="153"><col width="153"><col width="153"><col width="153"></colgroup>
<tbody>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-OzsodVs2eoDSLLxQuwEcf5jun4e"><strong>数据源</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-EnbcdfSyToOppAxhWkocmtY6nNh"><strong>适用场景</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-QlpIdMi9HoM6N5xGlpZcyyAInVe"><strong>优点</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-J3vMd4zZLociqjxAYksccDhUnec"><strong>缺点</strong></div>
</td>
</tr>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-FwS7dhXTeoJAZKxitZhcHm9CnXg"><strong>@ValueSource</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-XQpQdl9lcotELuxUchTccgVQnUe">基本数据类型简单测试</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-FVLZd5CRcojgMgxP5D2cK6FCnrX">使用简单</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-YFh4dfcQtoIGkMxdP8HcVuyYnVe">不支持复杂对象</div>
</td>
</tr>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-QeqMdU5J9oNKx3xGHMAcuvGenBb"><strong>@EnumSource</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-ND6kdjo6ootxuFxWEiqcT2KWn5f">枚举值测试</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-SGurd5C6locHknx9Sv7c3JeSnPh">自动生成所有枚举用例</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-Zn0jdcVwgo3O9Tx4sjQcPOaNned">仅适用于枚举</div>
</td>
</tr>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-LlrFdB6L9ofRbjxUzkmcVRvcn4d"><strong>@CsvSource</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-YtOIdMcXYoLBuExU5h9cGPpBnvg">结构化多参数测试</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-FWzudmjEboTyykx7e5kcfYpInbh">可读性好</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-BkBNdCMR8oAcdXxLo0kcPY7EnMb">维护大量数据时代码臃肿</div>
</td>
</tr>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-BegKd6P4Voff23x57JBcSdkPnyL"><strong>@CsvFileSource</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-POjMd9eFSoR3Ilx0lIycCpqLnc3">大量测试数据</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-PG2YdIWzeos6WNxRfT9cead1nk1">数据与代码分离</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-XHMedIYfqo29lHxfqPAcg9rRnN1">需要维护外部文件</div>
</td>
</tr>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-PEB6dQTPyo7s1oxFqIvcjhW8nSf"><strong>@MethodSource</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-RFsDdMjkaoleQyxQvkrcOmyqnce">需要动态生成或复杂对象的测试</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-AP71d9H72ouZD3xwuttcHfXmnaf">最灵活,支持任意数据类型</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-ENTVdkhNooa846xJGvFcNH77nJh">需要额外编写提供方法</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="ace-line ace-line old-record-id-Jc9dd8cJ5oXiHkxr4JvcyS1DnDf"> </div>
<h2 class="heading-2 ace-line old-record-id-Ou7pdJHg3ojKmzxtx7lc9OHunph">参数化测试的高级用法:自定义参数提供器</h2>
<div class="ace-line ace-line old-record-id-Waehd10WdoZHE3x2Sfrch5tvnwg">对于更复杂的场景,可以自定义参数提供器:</div>
<pre class="ace-line ace-line old-record-id-D5Y5dBn00oWpQWxxcqtchuOVnoh"><code class="language-Java" data-lark-language="Java" data-wrap="false">@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
static class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("apple", "banana").map(Arguments::of);
}
}</code></pre>
<div class="zoneType-calloutBlock old-record-id-U5k2dA209oP7Uyxm37Kced7nnMh">
<div class="callout-container" data-emoji-id="art">
<div class="callout-block">
<div class="ace-line ace-line old-record-id-PvaXdsP9noOmtlxTir9ctcH3nzh">另可参考博客:https://www.baeldung.com/parameterized-tests-junit-5</div>
</div>
</div>
</div>
<h1 class="heading-1 ace-line old-record-id-VO4Pd9mfhoqL5QxTdyrc8Mm9nIf">条件测试</h1>
<h2 class="heading-2 ace-line old-record-id-Rwiyd52ymolARmx8yRacVNQOnJc">适用场景</h2>
<div class="ace-line ace-line old-record-id-Pprade4OhoYbvixzjnuckMfhnRc">在实际项目中,有些测试可能只在特定条件下才需要运行,例如:</div>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-D1REd5Q1goEm4MxjQaWc3BKgnRh" data-list="bullet">只在特定操作系统上运行的测试</li>
<li class="ace-line ace-line old-record-id-GYardJ1PPoeMXKxxfhacBCcone1" data-list="bullet">需要特定环境变量或系统属性的测试</li>
<li class="ace-line ace-line old-record-id-IBAEd59k6ohdALxUwdQcbgMCnDf" data-list="bullet">只在集成测试环境中运行的耗时测试</li>
<li class="ace-line ace-line old-record-id-EnBMdcruGovRcVxUBjfcGA2mngd" data-list="bullet">针对特定租户的测试</li>
<li class="ace-line ace-line old-record-id-BQKjdBWXOoQUDzxvcRVc5EZSnSd" data-list="bullet">...</li>
</ul>
<div class="ace-line ace-line old-record-id-YlgFdaBNAom6nWxXJLccMP0anvb">JUnit 5的条件测试功能允许我们基于各种条件来启用或禁用测试,从而提高测试的灵活性和针对性。</div>
<h2 class="heading-2 ace-line old-record-id-ZEQ0d4xiqoW5aKxreEac3pbonme">内置注解</h2>
<div class="ace-line ace-line old-record-id-MOPGd4D5oopqZJx9cBHcghv5nob">JUnit 5提供了多种条件测试注解:</div>
<pre class="ace-line ace-line old-record-id-LkvrduzX8oOrnEx5MXZcbpdinwq"><code class="language-Java" data-lark-language="Java" data-wrap="false">@Test
@EnabledOnOs(OS.LINUX)
void onlyOnLinux() {
// 只在Linux系统上运行
}
@Test
@DisabledOnJre(JRE.JAVA_8)
void notOnJava8() {
// 不在Java 8上运行
}
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitecture() {
// 只在64位架构上运行
}
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "ci")
void onlyOnCiServer() {
// 只在CI服务器上运行
}
@Test
@EnabledIf("customCondition")
void onlyIfCustomCondition() {
// 只在自定义条件满足时运行
}
boolean customCondition() {
return // 自定义条件逻辑;
}</code></pre>
<div class="ace-line ace-line old-record-id-SkV5dB70KoxQ4KxDUbRckdsDn4c"> </div>
<h2 class="heading-2 ace-line old-record-id-Wk97d7ywnobBbrxoTvhcrOSSn1e">使用示例</h2>
<div class="ace-line ace-line old-record-id-Lt4QdN0BAoB44Qxz5ymc4oRBnFh">考虑一个多租户系统的测试场景:</div>
<pre class="ace-line ace-line old-record-id-BBAPdfCElorZsZxB3k6cunLgnUh"><code class="language-Java" data-lark-language="Java" data-wrap="false">@Test
@EnabledForTenant("tenantA")
void testFeatureForTenantA() {
// 只对租户A运行的测试
}
@Test
@EnabledForTenant({"tenantA", "tenantB"})
void testFeatureForMultipleTenants() {
// 对租户A和B运行的测试
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(EnabledForTenantCondition.class)
public @interface EnabledForTenant {
String[] value();
}
public class EnabledForTenantCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
Optional<EnabledForTenant> annotation = context.getElement()
.map(e -> e.getAnnotation(EnabledForTenant.class));
if (annotation.isPresent()) {
String currentTenant = System.getProperty("tenant");
if (Arrays.asList(annotation.get().value()).contains(currentTenant)) {
return ConditionEvaluationResult.enabled("租户匹配");
}
return ConditionEvaluationResult.disabled("当前租户不匹配");
}
return ConditionEvaluationResult.enabled("无租户限制");
}
}</code></pre>
<div class="ace-line ace-line old-record-id-Q3rrdAJlaoaMJ8xJ7eYcqeFpnVd"> </div>
<div class="ace-line ace-line old-record-id-SNzrdMPMQo4aByx1scdcJB54nof"> </div>
<h1 class="heading-1 ace-line old-record-id-D6i0dRQiOoKCIbx56qScVmjjnmc">使用Lambda表达式</h1>
<h2 class="heading-2 ace-line old-record-id-A33td4cFUoXwCzx1y4Icx8BFnuh">Lambda表达式在单元测试中的优势</h2>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-EpGmdUjeZo5ePkxCFF5c2Hi5ned" data-list="bullet">延迟计算:断言消息可以延迟计算,只有断言失败时才计算消息字符串,提高性能</li>
<li class="ace-line ace-line old-record-id-R71KdIpbvoEWiuxhpoHcA5p4nmc" data-list="bullet">简洁性:简化了异常断言等场景的代码</li>
<li class="ace-line ace-line old-record-id-VCxpdNtjhoYfywxIbpdcI98SnRe" data-list="bullet">灵活性:可以轻松实现自定义断言逻辑</li>
</ul>
<h2 class="heading-2 ace-line old-record-id-BI57dHGT2ocHbvxZGCOcWCgjnLh">应用场景</h2>
<div class="ace-line ace-line old-record-id-QlXudz6PuoTxzSxwNMmc6kB3nVn">断言消息延迟计算:</div>
<pre class="ace-line ace-line old-record-id-TvCRdEpofoTIWZx23KScZQE6nDW"><code class="language-Java" data-lark-language="Java" data-wrap="false">@Test
void testComplexObject() {
ComplexObject obj = service.createComplexObject();
assertNotNull(obj, () -> "创建的对象不应为null");
assertEquals("expected", obj.getValue(),
() -> String.format("对象值不匹配,实际值:%s", obj.getValue()));
}</code></pre>
<div class="ace-line ace-line old-record-id-IbSbdBsDGoUdC3xwHamcOj1inzb"> </div>
<div class="ace-line ace-line old-record-id-ZS2tdH7b9oTGQ9xDDJccWrhFnAd">异常断言:</div>
<pre class="ace-line ace-line old-record-id-XyhZdqfNnoLvx1xUsZ6caCb4ngc"><code class="language-Java" data-lark-language="Java" data-wrap="false">@Test
void testException() {
Executable executable = () -> service.methodThatShouldThrow();
assertThrows(ExpectedException.class, executable,
() -> "方法应抛出ExpectedException");
}</code></pre>
<div class="ace-line ace-line old-record-id-CUEidjtDdosfEzxpi9DceKDvnSh"> </div>
<div class="ace-line ace-line old-record-id-VL3md71kCoQeNQxjAdacPBsWnie">集合断言:</div>
<pre class="ace-line ace-line old-record-id-BwUrdkuv3ojlwOxpwObcEv7nnhx"><code class="language-Java" data-lark-language="Java" data-wrap="false">@Test
void testCollection() {
List<String> result = service.getItems();
assertAll("集合验证",
() -> assertFalse(result.isEmpty(), "集合不应为空"),
() -> assertTrue(result.contains("expected"), "应包含'expected'"),
() -> assertEquals(3, result.size(), "集合大小应为3")
);
}</code></pre>
<div class="ace-line ace-line old-record-id-JwLLdwpVOoSAPux4hm4cE4R4nlc"> </div>
<h2 class="heading-2 ace-line old-record-id-TVxgdCQQ5oAVZAxaGqzcryytndc">注意事项</h2>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-EkdZdWFF4oT85gx9UItcTgQKnic" data-list="bullet">保持简洁:Lambda表达式应保持简短,复杂逻辑应提取到单独方法中。</li>
<li class="ace-line ace-line old-record-id-Af5BdW2eQoUjAfxgjW5cMa6Enoc" data-list="bullet">合理命名:对于复杂的断言逻辑,可以使用方法引用或命名Lambda。</li>
<li class="ace-line ace-line old-record-id-FX9gdYRjcoHqDExeIDGczQBlnfc" data-list="bullet">避免副作用:Lambda中不应修改测试状态。</li>
<li class="ace-line ace-line old-record-id-TLHzdeG2noOVNIx7qv0cKTIanue" data-list="bullet">平衡可读性:不要过度使用Lambda,有时传统写法更易读。</li>
</ul>
<div class="ace-line ace-line old-record-id-IdlJd0NfboPs5cxDjFGcCP3nnJh"> </div>
<h1 class="heading-1 ace-line old-record-id-OIV2dHnsQoOIM7xFM8ocv90znTd">动态测试(DynamicTest)</h1>
<div class="ace-line ace-line old-record-id-E0u0dnNTLo3YY1xTzfQcPG4GnHh">动态测试也是 JUnit 5 中引入的一种新的编程模型。上文介绍的案例均使用 @Test 注解,属于编译时完全指定的静态测试。而动态测试<strong>是在运行时生成的测试</strong> 。这些测试由使用 @TestFactory 注解的工厂方法生成。</div>
<h2 class="heading-2 ace-line old-record-id-ZjH6dmqy5oDRl3x4SsGcCaCxnic">适用场景</h2>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-GHKhd10m6ohe5fxSd01cYD20n2d" data-list="bullet">测试数据需要从外部源动态获取时</li>
<li class="ace-line ace-line old-record-id-DslHdxopSoqRAaxDT4ycKWx4nlb" data-list="bullet">需要测试多种参数组合时</li>
<li class="ace-line ace-line old-record-id-Ijswd6zeFofSrgx2MT4czRY5n0b" data-list="bullet">测试用例需要根据环境条件动态生成时</li>
<li class="ace-line ace-line old-record-id-W9yqdwt5aoNsOrx2PDIcgnRwnte" data-list="bullet">需要对大量相似但不同的对象进行测试时</li>
<li class="ace-line ace-line old-record-id-XeQTddM5LouVC5xhM6kcnHv9nuh" data-list="bullet">需要构建复杂测试层次结构时</li>
</ul>
<h2 class="heading-2 ace-line old-record-id-F5Yzdz3xtogBLexevXac18NAnog">使用示例</h2>
<h3 class="heading-3 ace-line old-record-id-CWxEdjvLmoBYjexoo7ncSlcEnpg">运行时数据驱动的测试</h3>
<div class="ace-line ace-line old-record-id-ER66d3UT8oyBkbx6fU0cRmyEnNh">当测试数据需要在运行时动态获取(如数据库查询、文件读取、API响应等)</div>
<pre class="ace-line ace-line old-record-id-PzKPdV0CeoKW7LxKq8Uc9zimnbc"><code class="language-Java" data-lark-language="Java" data-wrap="false">@TestFactory
Stream<DynamicTest> databaseRecordTests() {
// 从数据库动态获取测试数据
List<Product> products = productRepository.findAllActive();
return products.stream()
.map(product -> dynamicTest(
"测试产品ID: "product.getId(),
() -> {
assertNotNull(product.getName());
assertTrue(product.getPrice() > 0);
assertFalse(product.isExpired());
}
));
}</code></pre>
<h3 class="heading-3 ace-line old-record-id-T5qzdXMBHoyW6HxNNg4ckKvUnFb">多维度组合测试</h3>
<div class="ace-line ace-line old-record-id-RFpZdAWf3od585xmcJNcoBw4nTb">测试多个参数的组合情况(例子:浏览器×操作系统×分辨率)</div>
<pre class="ace-line ace-line old-record-id-MFz2dLOXxoy4urxdTqpcDn9enae"><code class="language-Java" data-lark-language="Java" data-wrap="false">@TestFactory
Stream<DynamicTest> crossBrowserTests() {
List<String> browsers = Arrays.asList("Chrome", "Firefox", "Safari");
List<String> osList = Arrays.asList("Windows", "macOS", "Linux");
List<String> resolutions = Arrays.asList("1920x1080", "1366x768", "800x600");
return browsers.stream()
.flatMap(browser -> osList.stream()
.flatMap(os -> resolutions.stream()
.map(res -> dynamicTest(
browser + " on "os + " @"res,
() -> testCompatibility(browser, os, res)
))
)
);
}</code></pre>
<div class="ace-line ace-line old-record-id-BeCgdUWSLohxJpxoE0wc8Blrnrh"> </div>
<h3 class="heading-3 ace-line old-record-id-VRcdd1ez8oRTPKxIuwKcnvtinlf">条件测试生成</h3>
<div class="ace-line ace-line old-record-id-IvYfdIYRDobe9jxw9K9cCWesnT3">根据运行时环境动态生成不同的测试用例。</div>
<pre class="ace-line ace-line old-record-id-Rjevd6hvdo3sV2xB5aoctt5mnQf"><code class="language-Java" data-lark-language="Java" data-wrap="false">@TestFactory
Stream<DynamicTest> environmentSpecificTests() {
Stream.Builder<DynamicTest> builder = Stream.builder();
// 基础测试(所有环境都执行)
builder.add(dynamicTest("基本功能测试", this::testBasicFunctionality));
// 仅预发布环境执行的测试
if ("pre".equals(System.getenv("APP_ENV"))) {
builder.add(dynamicTest("预发布环境专有测试", this::testProductionFeature));
}
// 当有GPU时才执行的测试
if (GraphicsEnvironment.isHeadless()) {
builder.add(dynamicTest("GPU加速测试", this::testGpuAcceleration));
}
return builder.build();
}</code></pre>
<div class="ace-line ace-line old-record-id-HsUydoASfoNT1vxqClRcfK7QnCd"> </div>
<h3 class="heading-3 ace-line old-record-id-SdpidjNHYoilroxemavcElPonmf">性能基准测试/渐进式测试</h3>
<div class="ace-line ace-line old-record-id-EyTld8wViosvyPxSb2Qcb7bZnVf">如不同负载级别下的性能测试。</div>
<pre class="ace-line ace-line old-record-id-WDZ7dyWTho1ALHx74Nmc6uJfnWc"><code class="language-Java" data-lark-language="Java" data-wrap="false">@TestFactory
Stream<DynamicTest> performanceBenchmarks() {
intloadLevels = {100, 1000, 5000, 10000};
return Arrays.stream(loadLevels)
.mapToObj(load -> dynamicTest(
load + "并发请求负载测试",
() -> {
long startTime = System.nanoTime();
simulateLoad(load);
long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
assertTrue(duration < getThresholdFor(load),
() -> String.format("%d请求耗时%dms超过阈值", load, duration));
}
));
}</code></pre>
<div class="ace-line ace-line old-record-id-DniOdVxDroUlH3xLBJYcHbQCn3g"> </div>
<h3 class="heading-3 ace-line old-record-id-MnIQdAG8EoQe04xhEubcFd1bn6d">动态测试容器</h3>
<div class="ace-line ace-line old-record-id-H03Jd6p5comAIox6j1PcJ9eUnXd">适用于需要分层组织的复杂测试场景。</div>
<pre class="ace-line ace-line old-record-id-Fl7HdnF4AoRUS8xSNXbcrSW5ncb"><code class="language-Java" data-lark-language="Java" data-wrap="false">@TestFactory
Stream<DynamicNode> hierarchicalAPITests() {
return Stream.of(
DynamicContainer.dynamicContainer("用户API",
Stream.of(
dynamicTest("创建用户", this::testUserCreation),
dynamicTest("查询用户", this::testUserQuery)
)),
DynamicContainer.dynamicContainer("订单API",
Stream.of(
dynamicTest("创建订单", this::testOrderCreation),
DynamicContainer.dynamicContainer("支付流程",
Stream.of(
dynamicTest("支付请求", this::testPaymentRequest),
dynamicTest("支付回调", this::testPaymentCallback)
)
)
))
);
}</code></pre>
<div class="ace-line ace-line old-record-id-RROCdQE5loQlA3xx67dcoNA5nVc"> </div>
<h3 class="heading-3 ace-line old-record-id-Zy3tdUla5of8gRxTqPAc7BpNnnd">条件过滤测试</h3>
<div class="ace-line ace-line old-record-id-NJiKdCWjVo4EJUxin5pcbFhrnKj">例如根据不同条件对同一组输入数据执行不同测试逻辑时。</div>
<pre class="ace-line ace-line old-record-id-BJTudWhk6oAGXtxm9hucAsq5nch"><code class="language-Java" data-lark-language="Java" data-wrap="false">@TestFactory
Stream<DynamicTest> employeeWorkflowTests() {
// 测试数据准备
List<Employee> employees = Arrays.asList(
new Employee(1, "Fred"),// 有firstName的员工
new Employee(2), // 无firstName的员工(测试边界条件)
new Employee(3, "John") // 有firstName的员工
);
EmployeeService service = new EmployeeService();
// 测试流1:测试基础保存功能(所有员工)
Stream<DynamicTest> basicSaveTests = employees.stream()
.map(emp -> dynamicTest(
"基础保存-员工ID: "emp.getId(),
() -> {
Employee saved = service.save(emp.getId());
assertEquals(emp.getId(), saved.getId());
}
));
// 测试流2:测试带名称的保存功能(仅过滤有firstName的员工)
Stream<DynamicTest> namedSaveTests = employees.stream()
.filter(emp -> emp.getFirstName() != null && !emp.getFirstName().isEmpty())
.map(emp -> dynamicTest(
"带名称保存-"emp.getFirstName(),
() -> {
Employee saved = service.save(emp.getId(), emp.getFirstName());
assertEquals(emp.getId(), saved.getId());
assertEquals(emp.getFirstName(), saved.getFirstName());
}
));
// 合并两个测试流
return Stream.concat(basicSaveTests, namedSaveTests);
}</code></pre>
<div class="ace-line ace-line old-record-id-GCvkdswzIoUxsjxi6HvcNNOWnFH"> </div>
<h2 class="heading-2 ace-line old-record-id-Eldydd0mVo9GX3xH4rycMkpln6b">小结</h2>
<div class="ace-line ace-line old-record-id-SHgrd8QJlokDdzxHaPjc0l1jnif">通过以上例子,相信大家应该感受到了动态测试的灵活性,它:</div>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-FbR5dsjGHoMuGpxqXc9ctTgan9c" data-list="bullet">适用于需要针对同一数据集执行不同验证规则的场景</li>
<li class="ace-line ace-line old-record-id-SoSddUFGTo9fI9xJQAwcxcGLnNh" data-list="bullet">特别适合验证多版本API的兼容性</li>
<li class="ace-line ace-line old-record-id-QcP0dKE76oq6GzxANrOcqut4nRh" data-list="bullet">可扩展用于权限分级测试</li>
</ul>
<div class="ace-line ace-line old-record-id-YDNEduBN8oILyAxEAMAcOizcnUc"> </div>
<h2 class="heading-2 ace-line old-record-id-Hlp2djQmnorscoxVOpScHTOtnsd">注意事项</h2>
<h3 class="heading-3 ace-line old-record-id-B3audOoRkoRK9MxalCBcDWncnGc">工厂方法声明</h3>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-FX4LdBiUyo4RAGxLBFJcURF5nGe" data-list="bullet">注解要求:必须使用@TestFactory明确标记方法,以指示该方法为动态测试工厂</li>
<li class="ace-line ace-line old-record-id-ArqhdIc5uoYWuExU6y1caTuonhc" data-list="bullet">方法签名:与静态测试方法不同,工厂方法必须声明返回测试实例集合</li>
</ul>
<h3 class="heading-3 ace-line old-record-id-D7DrdBFW8oDBZSxqHdrcPc6pn8d">返回类型限制</h3>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-URxWdgNxFoJIvXxmNaYcPw4pnXe" data-list="bullet">合法返回类型:仅允许以下集合类型:</li>
</ul>
<pre class="ace-line ace-line old-record-id-MhmtdOvVaoHpCrxml0Zc0jGqn3f"><code class="language-Java" data-lark-language="Java" data-wrap="false">Stream<DynamicTest>// 最常用的流式处理
Collection<DynamicTest>// 基础集合类型
Iterable<DynamicTest>// 可迭代接口
Iterator<DynamicTest>// 迭代器实现</code></pre>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-B3J5dwRz9osIKZxgnVRcsUODntd" data-list="bullet">类型安全:JUnit 5会在运行时验证返回类型,非法类型将抛出JUnitException</li>
</ul>
<h3 class="heading-3 ace-line old-record-id-R0o9doaojoKrdYxubsXc8YZunDd">方法修饰符约束</h3>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-IyGmdJCcco8cDGxeozuc10S9nUe" data-list="bullet">禁止修饰符private和 static 。换句话说,方法必须能够访问测试类实例状态(非static)且对框架可见(非private)</li>
</ul>
<h3 class="heading-3 ace-line old-record-id-FRUAdhDeDoWS6nxAtRccVHSynVb">生命周期回调差异</h3>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-Xx8VdNA7yog5WlxfhwrcuHJtnPg" data-list="bullet">不支持的回调:</li>
</ul>
<pre class="ace-line ace-line old-record-id-GO8adWsq9oWlm4x3WHYcUtK8nMb"><code class="language-Java" data-lark-language="Java" data-wrap="false">@BeforeEach // 每个动态测试前不会执行
@AfterEach// 每个动态测试后不会执行</code></pre>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-IvBWdGINJoD9IFxlYkIc0rkin1U" data-list="bullet">替代方案:需在动态测试内部自行管理初始化和清理逻辑,例如:</li>
</ul>
<pre class="ace-line ace-line old-record-id-ZszLdGliAoo5DXx6tZlcdRH8nLg"><code class="language-Java" data-lark-language="Java" data-wrap="false">dynamicTest("自定义生命周期", () -> {
// 初始化代码
try {
// 测试逻辑
} finally {
// 清理代码
}
})</code></pre>
<div class="ace-line ace-line old-record-id-RpK6d2gu3oul3QxWbLIcoyfWn7d"> </div>
<h2 class="heading-2 ace-line old-record-id-DNKSdxi9RomkjQxxebnctbZ8nUh">与静态测试的关键区别</h2>
<div>
<table class="ace-table" data-ace-table-col-widths="112;190;173"><colgroup><col width="112"><col width="190"><col width="173"></colgroup>
<tbody>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-Lfq0dRf3rozG42xnoLNcTsALnWb"><strong>特性</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-NYaRdmZ6hoKs9Tx6NdrcKLesnFd"><strong>动态测试</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-XYWVdS6Bgo0b4NxU78ocIIRznDh"><strong>静态测试</strong><strong> (@Test)</strong></div>
</td>
</tr>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-BkJvdq4fkoum9ux0NWhcDA2gnFc"><strong>生成时机</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-GnO9dW5JCoquPoxXyZMcUesHnob">运行时动态生成</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-L1DmdEDCaokMhDxJbsncjpHJnrc">编译时静态定义</div>
</td>
</tr>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-HIVod7wQioThpqxKaOMcfMpGnvf"><strong>用例独立性</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-ADXRdN1p6oBwJ8xLfqGca0k6nUc">每个测试完全独立</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-Q8n9dWMJdoB3XBx4PYtc3zu1nCc">共享相同测试方法体</div>
</td>
</tr>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-Gs3bdkhbqoElLtxMhnYcwNsMnAg"><strong>组织结构</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-QL9QdVHShoMxH9xrFdyc6mJ3nFf">支持嵌套容器复杂结构</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-NJaSdTrNlo4S4LxafNJcyPlqn8g">仅支持平面结构</div>
</td>
</tr>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-IAQYdPCrYovPoNx9SeccFSndnKf"><strong>生命周期支持</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-CtLydhUJzopMD5xGCeMcDqNIn3e">无自动回调</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-LwZld3eQYo5da7xpKMfcAam6ndb">完整生命周期支持</div>
</td>
</tr>
<tr>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-Q1hjdIHPFoH4nExwxofcO2dnn4b"><strong>数据驱动方式</strong></div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-EAJDdqmBkoqYDJxAxcocWdjLn7g">完全自由的数据生成</div>
</td>
<td rowspan="1" colspan="1">
<div class="ace-line ace-line old-record-id-YfzddOIg2oNPtXx0L6hcBN9FnJd">通过参数化注解限定</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="zoneType-calloutBlock old-record-id-E5LLdgphBojuW7x6CnMcoX5bnaf">
<div class="callout-container" data-emoji-id="first_place_medal">
<div class="callout-block">
<div class="ace-line ace-line old-record-id-KHnodfaPRoL3yHxeHTDcj1MqnDh">另可参考博客:https://www.baeldung.com/junit5-dynamic-tests</div>
</div>
</div>
</div>
<div class="ace-line ace-line old-record-id-AorqdbzZLocD1kxxv7acOFYFnWb"> </div>
<h1 class="heading-1 ace-line old-record-id-BNvddhqtUojOIjxIVGXcBn5GnNe">总结</h1>
<div class="ace-line ace-line old-record-id-JF0edGrlkoqtbmxhn8ncrI2onzh">JUnit 5提供了一系列强大的高级特性,可以显著提升单元测试的质量和效率:</div>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-Roq9dfodpoJ29NxJ29TczfBGnjh" data-list="bullet">@DisplayName 使测试报告更加可读,便于团队沟通和维护</li>
<li class="ace-line ace-line old-record-id-BWQidcb72ogdg6xcT4QcWuxNnRf" data-list="bullet">@Nested 帮助组织复杂测试场景,呈现清晰的测试结构</li>
<li class="ace-line ace-line old-record-id-NevOdsr6noFuvexZKusclPWmnZe" data-list="bullet">@RepeatedTest 简化幂等性和稳定性测试</li>
<li class="ace-line ace-line old-record-id-Aj39dDKRaokLRDxlV0bclw1on5c" data-list="bullet">参数化测试 减少重复代码,提高测试覆盖率</li>
<li class="ace-line ace-line old-record-id-KnkodyNXwoi1i1x8BBicd5Xpn0f" data-list="bullet">条件测试 使测试更加灵活,适应不同环境需求</li>
<li class="ace-line ace-line old-record-id-UTurdz3n9oTFkzx3zIect6xjnMf" data-list="bullet">Lambda表达式 使断言更加简洁和高效</li>
<li class="ace-line ace-line old-record-id-OyrvdOksRoxLzvxCi21cRoRSn8e" data-list="bullet">动态测试赋予了更高的灵活性</li>
</ul>
<div class="ace-line ace-line old-record-id-DnhKdSkanofo5WxqfhqchYLwnlV">以上这些特性不是孤立的,它们可以组合使用,创造出更加强大和灵活的测试解决方案。例如,你可以创建一个参数化的嵌套测试,使用条件执行,并在断言中使用Lambda表达式。</div>
<div class="ace-line ace-line old-record-id-AkcjdnUoVobVmMxExSMcR19Bnmh">在实际项目中,建议根据项目特点逐步引入这些高级特性,不必一次性全部采用。从@DisplayName和@Nested开始改善测试可读性,然后根据需要引入参数化测试和条件测试等更高级的功能。</div>
<div class="ace-line ace-line old-record-id-DkHTdEo5tomnUzxqwa0cVkq4n5c">通过合理运用JUnit 5的这些高级特性,我们可以编写出更优雅、更易维护的单元测试,从而更有效地保障代码质量,提高开发效率。</div>
<div class="ace-line ace-line old-record-id-FHi8dFzs2oeVVyxsE6EcVXLlnMM"> </div>
<h1 class="heading-1 ace-line old-record-id-TOP8dmp1do2eQZx8OEicflN7ncg">后记</h1>
<ul class="list-bullet1">
<li class="ace-line ace-line old-record-id-Y8Lbd6k4aotpfPx4pZFcSyLYn6e" data-list="bullet">JUnit 的官方文档非常全面,可以当做字典来用:https://junit.org/junit5/docs/5.1.0/user-guide/。</li>
<li class="ace-line ace-line old-record-id-WP8KdtnvpoJZrdxcrCbcSmZNntc" data-list="bullet">JUnit(包括4和5)本身是一个非常优秀的框架,大家有精力可以看看源码。比如JUnit 使用了多种经典设计模式,如工厂模式、装饰器模式、组合模式、职责链模式、模板方法模式、观察者模式等,核心代码量不大但功能完备,且扩展性很强。</li>
<li class="ace-line ace-line old-record-id-XIQ8de2QdoyjkfxNCmocLK6CnBb" data-list="bullet">有兴趣的读者,可以去学习一下 TDD 测试驱动开发的理念,这种开发模式与现有的模式正好是反过来的,可以有效提升代码质量(但有一定的学习成本,推行难度也较大)。</li>
</ul>
<div class="ace-line ace-line old-record-id-INnadvlACoNo6bxjSmKcMLvynUf"> </div>
</div>
</div>
<div id="MySignature" role="contentinfo">
『注:本文来自博客园“小溪的博客”,若非声明均为原创内容,请勿用于商业用途,转载请注明出处http://www.cnblogs.com/xiaoxi666/』<br><br>
来源:https://www.cnblogs.com/xiaoxi666/p/18935987
頁:
[1]