Junit5参数化实战,让测试更优雅

转载请注明出处❤️

作者:测试蔡坨坨

原文链接:caituotuo.top/b0dfc061.html


前言

你好,我是测试蔡坨坨。

在代码的世界里,有一片自动化的花园,那里的用例是微风吹拂下的花朵,绽放着不同的颜色。在这片花园中,我们常常遇到一个美妙的情景:相同的测试流程,却需要随着业务的风向,切换不同的测试数据。这就像是一支曲子,相同的旋律,却因音符的不同而显得迥然不同。

就如诗人所言,方法的舞步相同,只是入参的音符不同。我们需要思考等价的类别,探寻边界的价值,从而谱写出一曲动人心弦的测试乐章。

然而,如果把所有的测试数据都堆砌在方法中,就像是在花园里撒下过多的种子,反而显得杂乱无章。那用例的可维护性和可阅读性,就如同被昏暗的雾霭遮掩了一般。

而在这个代码的诗坛上,各路诗人都创造了解决方案的花园。就如音乐家有琴键与弦线,TestNG 有 @Parameters 和 @DataProvider,Pytest 也拥有 @pytest.mark.parametrize 等乐符,为我们奏响了测试的乐章。

当然,Junit也为我们提供了一套卓越的解决方案,让参数化用例的编写变得更加优雅。这项特性使得我们能够以一种优美的方式,运行单个测试多次,每次运行仅仅参数有所不同。更妙的是,每条测试用例都能够独立存在,彼此之间毫不干扰。

在这篇文章中,我将带领大家深入体验一下Junit5是如何实现参数化的奇妙之处。让我们一同踏上这段探索之旅,领略代码世界的多彩风景。

Junit5 参数化

Junit5参数化的魅力令人为之倾倒,其使用之便捷简直令人惊叹。只需嵌入少许注解,便能开启一场多维数据之旅,而数据的来源更是多姿多彩:单参数、多参数、甚至文件中的数据、方法所提供的数据,无一不在其考虑之列。这一巧妙设计,为测试带来了前所未有的灵活性与丰富性。

官方文档:https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

安装依赖

欲使用Junit5的参数化,需要在Junit Platform的基础上导入junit-jupiter-params依赖包。

如果你的项目是使用Maven构建,那么只需要在pom文件中引入以下依赖即可:

1
2
3
4
5
6
7
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>

单参数 @ValueSource

@ValueSource 是最简单的参数化方式,它允许往测试方法中传递一个数据或者迭代器。

支持以下类型的单参数数据的参数化:

参数 参数类型
shorts short
bytes byte
ints int
longs long
floats float
doubles double
chars char
booleans boolean
strings java.long.String
classes java.long.Class
使用步骤
  • 使用参数化用例时,需将@Test注解换成@ParameterizedTest

  • 添加单参数化注解@ValueSource

  • 注意:如果@Test和@ParameterizedTest同时使用,则会多执行一次,且由于@Test无法传递参数,所以运行时会报ParameterResolutionException异常

实战演练

为方便演示,下面将使用一道算法题实现的功能作为被测对象,进行参数化用例的实战演练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package top.caituotuo.demo;

import java.util.HashMap;
import java.util.Map;

/**
* author: 测试蔡坨坨
* datetime: 2023-8-21 02:12:43
* function: 给定一个字符串,找出不含有重复字符的最长子串的长度。
* 例如:
* 给定 "abcabcbb" ,没有重复字符的最长子串是 "abc" ,长度为 3。
* 给定 "bbbbb" ,最长的子串就是 "b" ,长度是 1。
* 给定 "pwwkew" ,最长的子串是 "wke" ,长度是 3。
* 请注意答案必须是一个子串,"pwke" 是子序列 而不是子串。
*/

public class DemoTest {
public int lengthOfLongestSubstr(String s) {
if (s == null || s.length() == 0) {
return 0;
}
// 定义dp数组的含义:以字符s[i]结尾时,不重复的子串长度为dp[i]
int[] dp = new int[s.length()];
// 初始值
dp[0] = 1;
Map<Character, Integer> map = new HashMap<>();
map.put(s.charAt(0), 0);
int maxLen = 1;
int startIndex = 0;
// 状态转换关系式
for (int i = 1; i < s.length(); i++) {
if (!map.containsKey(s.charAt(i))) {
dp[i] = dp[i - 1] + 1;
} else {
int k = map.get(s.charAt(i));
dp[i] = i - k <= dp[i - 1] ? i - k : dp[i - 1] + 1;
}
// maxLen = Math.max(maxLen, dp[i]);
if (dp[i] > maxLen) {
maxLen = dp[i];
startIndex = i - maxLen + 1;
}
map.put(s.charAt(i), i);
}
System.out.printf("原字符串:%s,最长不重复子串:%s,长度:%s%n", s, s.substring(startIndex, startIndex + maxLen), maxLen);
return maxLen;
}
}

以String类型的单参数化举栗:

1
2
3
4
5
6
7
8
9
10
11
/**
* @param s 测试方法中声明形参,代表参数化通过这个形参给到测试方法去使用
*/
// @Test
// 将@Test注解换成@ParameterizedTest注解,指明参数化测试用例
@ParameterizedTest
// 单参数注解,示例中为String类型参数化
@ValueSource(strings = {"abcabcbb", "pwwkew"})
public void test(String s) {
assertEquals(3, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

从Junit5.4开始,可以使用@NullSource、@EmptySource和@NullAndEmptySource注解分别将单个null值、单个Empty值 和 null+Empty 作为参数传递给测试方法,如下示例:

1
2
3
4
5
6
@ParameterizedTest
@NullSource
@EmptySource
public void test0(String s) {
assertEquals(0, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

1
2
3
4
5
@ParameterizedTest
@NullAndEmptySource
public void test1(String s) {
assertEquals(0, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

多参数 @CsvSource

在诸多场景中,单一参数恐难以尽善尽美,往往需同时传入测试数据预期结果来验证测试逻辑是否符合预期。为此,多参数的参数化方式将至关重要。

还是用前面所说的算法题举栗,有以下两条用例:

  1. 给定 “abcabcbb” ,没有重复字符的最长子串是 “abc” ,长度为 3。
  2. 给定 “bbbbb” ,没有重复字符的最长子串是 “b” ,长度为 1。
使用步骤
  • 添加多参数参数化注解 @CsvSource
  • @CsvSource 通过默认或指定的分隔符实现参数化
实战演练
默认分隔符
1
2
3
4
5
6
@ParameterizedTest
// 传递的参数格式是一个集合,如果是多个参数,使用默认分隔符","分开
@CsvSource({"abcabcbb,3", "bbbbb,1"})
public void test2(String s, int n) {
assertEquals(n, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

指定分隔符

@CsvSource 的分隔符默认是逗号,在实际测试中,若逗号需要被当做参数进行传递,那么我们还可以使用delimiterString属性来自定义分割符号,如下示例:

1
2
3
4
5
6
@ParameterizedTest
// 使用delimiterString指定分隔符,使用value指定数据源
@CsvSource(value = {"abcabcbb|3", "bbbbb|1"}, delimiterString = "|")
public void test3(String s, int n) {
assertEquals(n, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

1
2
3
4
5
6
@ParameterizedTest
// 指定分隔符为“测试蔡坨坨”
@CsvSource(value = {"abcabcbb测试蔡坨坨3", "bbbbb测试蔡坨坨1"}, delimiterString = "测试蔡坨坨")
public void test4(String s, int n) {
assertEquals(n, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

多参数文件参数化 @CsvFileSource

实际测试中,CSV测试数据常存储在CSV文件之中,需要通过读取文件来获取测试数据。

此时就可以使用@CsvFileSource注解来指定文件路径,实现文件数据源的读取。

使用步骤
  • 添加多参数文件参数化注解 @CsvFileSource
  • 在项目的 test/resources 中新增测试数据 csv 文件
  • @CsvFileSource 支持指定分隔符进行参数化
实战演练

通常情况下,@CsvFileSource注解会去解析每一行,但有些时候第一行可能是列名,因此我们可以添加numLinesToSkip = 1属性来跳过第1行。同样与@CsvSource一样,也可以使用delimiterString来指定分隔符。

测试数据:

1
2
3
4
5
@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1, delimiterString = "-")
public void test5(String s, int n) {
assertEquals(n, new DemoTest().lengthOfLongestSubstr(s));
}

运行结果:

方法参数 @MethodSource

有时参数的来源颇非简单的数据结构,参数存储的文件也不一定是CSV文件,或者还有Excel、YAML等。

于是,这些错综复杂的数据结构欲化身为测试参数,需借助一些巧妙之法,将其读取转换为方法,并将方法作为参数传递给测试方法。

Junit5同样提供了妙不可言的解决方案,我们可以借助@MethodSource注解,传递复杂的迭代对象到测试方法中。@MethodSource使用非常灵活,既能从文件中提取,亦能从接口的返回值中提取。毕竟,其本质是以一个方法作为参数的来源,那么任何复杂的数据结构我们都可以在方法中做定制化处理。

使用步骤
  • 通过@MethodSource注解引用方法作为参数化的数据源信息,允许引用一个或多个测试类的工厂方法,这样的方法必须返回一个Stream,Iterable,Iterator或参数数组。另外,这种方法不能接受任何参数。
  • 在@MethodSource注解的参数必须是静态的工厂方法,除非测试类被注释为@TestInstance(Lifecycle.PER_CLASS)
  • 静态工厂方法的返回值需要和测试方法的参数对应
  • 如果在@MethodSource注解中未指明方法名,会自动调用与测试方法同名的静态方法
实战演练

如果只需要一个参数,则可以返回参数类型的实例Stream,如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package top.caituotuo.demo;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

/**
* author: 测试蔡坨坨
* datetime: 2023/8/26 16:38
* function: 单参数方法
*/
public class MethodSourceTest {
/**
* @param name 添加形参,形参的类型要和静态方法内部的元素类型一致
*/
@ParameterizedTest
// 通过@MethodSource注解指定数据源的方法名
@MethodSource("stringProvider")
void test1(String name) {
System.out.println(name);
}

/**
* 定义一个静态方法,提供参数化数据源
*
* @return 返回Stream流
*/
static Stream<String> stringProvider() {
return Stream.of("测试蔡坨坨", "小趴蔡", "IT小学生蔡坨坨");
}
}

运行结果:

支持原始类型(DoubleStreamIntStreamLongStream)的流,示例如下:

1
2
3
4
5
6
7
8
@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
assertNotEquals(9, argument);
}
static IntStream range() {
return IntStream.range(0, 20).skip(10);
}

如果测试方法声明多个参数,则需要返回一个集合或Arguments实例流,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package top.caituotuo.demo;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* author: 测试蔡坨坨
* datetime: 2023/8/22 23:32
* function: 给出一个包含3个数字的列表,判断此3个数字能否组成三角形。能的话输出true,不能的话输出false。
* 输入[3,4,3] 输出true
* 输入[1,2,3] 输出false
*/
public class DemoTest {
public boolean isTriangle(List<Integer> sides) {
if (sides.size() != 3) {
return false;
}

return sides.stream().allMatch(side -> side > 0) &&
IntStream.range(0, 3).allMatch(i ->
sides.get(i) + sides.get((i + 1) % 3) > sides.get((i + 2) % 3));
}

@ParameterizedTest
@MethodSource({"getTestSides"})
public void test(List<Integer> sides, boolean expectedResult) {
assertEquals(expectedResult, new DemoTest().isTriangle(sides));
}

static Stream<Arguments> getTestSides() {
return Stream.of(
Arguments.of(Arrays.asList(3, 4, 3), true),
Arguments.of(Arrays.asList(1, 2, 3), false),
Arguments.of(Arrays.asList(-3, 4, 5), false),
Arguments.of(Arrays.asList(3, 4, 5, 6), false)
);
}
}

运行结果:

关于 Junit5 参数化的探讨,暂时就聊到这里,我们将在下一期再度相聚。期待与你再次分享更多关于优雅测试的心灵之旅。愿我们的交流如同代码一般,不断升华。再会。